diff --git a/.github/workflows/toolshed.yaml b/.github/workflows/toolshed.yaml index cc54bf43c067..f6c06663da3c 100644 --- a/.github/workflows/toolshed.yaml +++ b/.github/workflows/toolshed.yaml @@ -20,14 +20,23 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7'] - test-install-client: ['standalone', 'galaxy_api'] - # v1 is mostly working... - shed-api: ['v1'] - # lets get twill working with twill then try to - # make progress on the playwright - # shed-browser: ['twill', 'playwright'] - shed-browser: ['playwright'] + include: + - test-install-client: 'galaxy_api' + python-version: '3.7' + shed-api: 'v1' + shed-browser: 'twill' + - test-install-client: 'standalone' + python-version: '3.8' + shed-api: 'v1' + shed-browser: 'twill' + - test-install-client: 'galaxy_api' + python-version: '3.9' + shed-api: 'v2' + shed-browser: 'playwright' + - test-install-client: 'standalone' + python-version: '3.10' + shed-api: 'v2' + shed-browser: 'playwright' services: postgres: image: postgres:13 diff --git a/.vscode/shed.code-snippets b/.vscode/shed.code-snippets new file mode 100644 index 000000000000..f90a37c71313 --- /dev/null +++ b/.vscode/shed.code-snippets @@ -0,0 +1,41 @@ +{ + "shedcomp": { + "prefix": "shed_component", + "body": [ + "", + "" + ], + "description": "outline of a tool shed component" + }, + "shedpage": { + "prefix": "shed_page", + "body": [ + "", + "" + ], + "description": "outline of a tool shed page" + }, + "shedfetcher": { + "prefix": "shed_fetcher", + "body": [ + "import { fetcher } from \"@/schema\"", + "const fetcher = fetcher.path(\"$1\").method(\"get\").create()" + ], + "description": "Import shed fetcher and instantiate with a path" + }, + "shedrouter": { + "prefix": "shed_router", + "body": [ + "import router from \"@/router\"" + ] + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index 0dd02e253c9d..f20548df435e 100644 --- a/Makefile +++ b/Makefile @@ -190,6 +190,7 @@ remove-api-schema: update-client-api-schema: client-node-deps build-api-schema $(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts + $(IN_VENV) cd client && node openapi_to_schema.mjs ../_shed_schema.yaml > ../lib/tool_shed/webapp/frontend/src/schema/schema.ts && npx prettier --write ../lib/tool_shed/webapp/frontend/src/schema/schema.ts $(MAKE) remove-api-schema lint-api-schema: build-api-schema diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index 4b851aed081a..dcc9ccb09e8c 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -73,6 +73,7 @@ fsspec==2023.1.0 ; python_version >= "3.7" and python_version < "3.12" future==0.18.3 ; python_version >= "3.7" and python_version < "3.12" galaxy-sequence-utils==1.1.5 ; python_version >= "3.7" and python_version < "3.12" galaxy2cwl==0.1.4 ; python_version >= "3.7" and python_version < "3.12" +graphene-sqlalchemy==3.0.0b3 ; python_version >= "3.7" and python_version < "3.12" gravity==1.0.3 ; python_version >= "3.7" and python_version < "3.12" greenlet==2.0.2 ; python_version >= "3.7" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "3.12" gunicorn==21.2.0 ; python_version >= "3.7" and python_version < "3.12" @@ -179,6 +180,7 @@ sqlalchemy==1.4.49 ; python_version >= "3.7" and python_version < "3.12" sqlitedict==2.1.0 ; python_version >= "3.7" and python_version < "3.12" sqlparse==0.4.4 ; python_version >= "3.7" and python_version < "3.12" starlette-context==0.3.5 ; python_version >= "3.7" and python_version < "3.12" +starlette_graphene3==0.6.0 ; python_version >= "3.7" and python_version < "3.12" starlette==0.27.0 ; python_version >= "3.7" and python_version < "3.12" supervisor==4.2.5 ; python_version >= "3.7" and python_version < "3.12" svgwrite==1.4.3 ; python_version >= "3.7" and python_version < "3.12" diff --git a/lib/galaxy/managers/api_keys.py b/lib/galaxy/managers/api_keys.py index 8e9b41c32380..c5584acc158b 100644 --- a/lib/galaxy/managers/api_keys.py +++ b/lib/galaxy/managers/api_keys.py @@ -3,7 +3,8 @@ TYPE_CHECKING, ) -from galaxy.model import User +from typing_extensions import Protocol + from galaxy.model.base import transaction from galaxy.structured_app import BasicSharedApp @@ -11,11 +12,15 @@ from galaxy.model import APIKeys +class IsUserModel(Protocol): + id: str + + class ApiKeyManager: def __init__(self, app: BasicSharedApp): self.app = app - def get_api_key(self, user: User) -> Optional["APIKeys"]: + def get_api_key(self, user: IsUserModel) -> Optional["APIKeys"]: sa_session = self.app.model.context api_key = ( sa_session.query(self.app.model.APIKeys) @@ -25,7 +30,7 @@ def get_api_key(self, user: User) -> Optional["APIKeys"]: ) return api_key - def create_api_key(self, user: User) -> "APIKeys": + def create_api_key(self, user: IsUserModel) -> "APIKeys": guid = self.app.security.get_new_guid() new_key = self.app.model.APIKeys() new_key.user_id = user.id @@ -36,7 +41,7 @@ def create_api_key(self, user: User) -> "APIKeys": sa_session.commit() return new_key - def get_or_create_api_key(self, user: User) -> str: + def get_or_create_api_key(self, user: IsUserModel) -> str: # Logic Galaxy has always used - but it would appear to have a race # condition. Worth fixing? Would kind of need a message queue to fix # in multiple process mode. @@ -44,7 +49,7 @@ def get_or_create_api_key(self, user: User) -> str: key = api_key.key if api_key else self.create_api_key(user).key return key - def delete_api_key(self, user: User) -> None: + def delete_api_key(self, user: IsUserModel) -> None: """Marks the current user API key as deleted.""" sa_session = self.app.model.context # Before it was possible to create multiple API keys for the same user although they were not considered valid diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 4489a00689e6..af50f93d7e1b 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -451,7 +451,9 @@ def change_password(self, trans, password=None, confirm=None, token=None, id=Non trans.sa_session.add(token_result) return user, "Password has been changed. Token has been invalidated." else: - user = self.by_id(self.app.security.decode_id(id)) + if not isinstance(id, int): + id = self.app.security.decode_id(id) + user = self.by_id(id) if user: message = self.app.auth_manager.check_change_password(user, current, trans.request) if message: diff --git a/lib/galaxy/webapps/base/webapp.py b/lib/galaxy/webapps/base/webapp.py index 8a4e4af3dd73..d56a2d878a55 100644 --- a/lib/galaxy/webapps/base/webapp.py +++ b/lib/galaxy/webapps/base/webapp.py @@ -735,21 +735,9 @@ def __create_new_session(self, prev_galaxy_session=None, user_for_new_session=No Caller is responsible for flushing the returned session. """ - session_key = self.security.get_new_guid() - galaxy_session = self.app.model.GalaxySession( - session_key=session_key, - is_valid=True, - remote_host=self.request.remote_host, - remote_addr=self.request.remote_addr, - referer=self.request.headers.get("Referer", None), + return create_new_session( + self, prev_galaxy_session=prev_galaxy_session, user_for_new_session=user_for_new_session ) - if prev_galaxy_session: - # Invalidated an existing session for some reason, keep track - galaxy_session.prev_session_id = prev_galaxy_session.id - if user_for_new_session: - # The new session should be associated with the user - galaxy_session.user = user_for_new_session - return galaxy_session @property def cookie_path(self): @@ -1106,6 +1094,31 @@ def qualified_url_for_path(self, path): return url_for(path, qualified=True) +def create_new_session(trans, prev_galaxy_session=None, user_for_new_session=None): + """ + Create a new GalaxySession for this request, possibly with a connection + to a previous session (in `prev_galaxy_session`) and an existing user + (in `user_for_new_session`). + + Caller is responsible for flushing the returned session. + """ + session_key = trans.security.get_new_guid() + galaxy_session = trans.app.model.GalaxySession( + session_key=session_key, + is_valid=True, + remote_host=trans.request.remote_host, + remote_addr=trans.request.remote_addr, + referer=trans.request.headers.get("Referer", None), + ) + if prev_galaxy_session: + # Invalidated an existing session for some reason, keep track + galaxy_session.prev_session_id = prev_galaxy_session.id + if user_for_new_session: + # The new session should be associated with the user + galaxy_session.user = user_for_new_session + return galaxy_session + + def default_url_path(path): return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index b38ef704a398..b6a44a1f43ac 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -47,6 +47,7 @@ NoMatchFound, ) from starlette.types import Scope +from typing_extensions import Literal try: from starlette_context import context as request_context @@ -200,6 +201,8 @@ class GalaxyASGIRequest(GalaxyAbstractRequest): Implements the GalaxyAbstractRequest interface to provide access to some properties of the request commonly used.""" + __request: Request + def __init__(self, request: Request): self.__request = request self.__environ: Optional[MutableMapping[str, Any]] = None @@ -232,6 +235,28 @@ def environ(self) -> MutableMapping[str, Any]: self.__environ = build_environ(self.__request.scope, None) # type: ignore[arg-type] return self.__environ + @property + def headers(self): + return self.__request.headers + + @property + def remote_host(self) -> str: + # was available in wsgi and is used create_new_session + return self.host + + @property + def remote_addr(self) -> Optional[str]: + # was available in wsgi and is used create_new_session + # not sure what to do here... + return None + + @property + def is_secure(self) -> bool: + return self.__request.url.scheme == "https" + + def get_cookie(self, name): + return self.__request.cookies.get(name) + class GalaxyASGIResponse(GalaxyAbstractResponse): """Wrapper around Starlette/FastAPI Response object. @@ -246,6 +271,31 @@ def __init__(self, response: Response): def headers(self): return self.__response.headers + def set_cookie( + self, + key: str, + value: str = "", + max_age: Optional[int] = None, + expires: Optional[int] = None, + path: str = "/", + domain: Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: Optional[Literal["lax", "strict", "none"]] = "lax", + ) -> None: + """Set a cookie.""" + self.__response.set_cookie( + key, + value, + max_age=max_age, + expires=expires, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + ) + DependsOnUser = cast(Optional[User], Depends(get_user)) diff --git a/lib/galaxy/webapps/galaxy/controllers/user.py b/lib/galaxy/webapps/galaxy/controllers/user.py index 193255a0b59a..fb9d7e11c5cf 100644 --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -188,7 +188,7 @@ def __validate_login(self, trans, payload=None, **kwd): message, status = self.resend_activation_email(trans, user.email, user.username) return self.message_exception(trans, message, sanitize=False) else: # activation is OFF - pw_expires = trans.app.config.password_expiration_period + pw_expires = getattr(trans.app.config, "password_expiration_period", None) if pw_expires and user.last_password_change < datetime.today() - pw_expires: # Password is expired, we don't log them in. return { diff --git a/lib/galaxy/work/context.py b/lib/galaxy/work/context.py index 310779e504e3..8a1206018c0a 100644 --- a/lib/galaxy/work/context.py +++ b/lib/galaxy/work/context.py @@ -4,6 +4,8 @@ Optional, ) +from typing_extensions import Literal + from galaxy.managers.context import ProvidesHistoryContext from galaxy.model import ( GalaxySession, @@ -85,6 +87,14 @@ def base(self) -> str: def host(self) -> str: """The host address.""" + @abc.abstractproperty + def is_secure(self) -> bool: + """Was this a secure (https) request.""" + + @abc.abstractmethod + def get_cookie(self, name): + """Return cookie.""" + class GalaxyAbstractResponse: """Abstract interface to provide access to some response utilities.""" @@ -102,6 +112,21 @@ def set_content_type(self, content_type: str): def get_content_type(self): return self.headers.get("content-type", None) + @abc.abstractmethod + def set_cookie( + self, + key: str, + value: str = "", + max_age: Optional[int] = None, + expires: Optional[int] = None, + path: str = "/", + domain: Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: Optional[Literal["lax", "strict", "none"]] = "lax", + ) -> None: + """Set a cookie.""" + class SessionRequestContext(WorkRequestContext): """Like WorkRequestContext, but provides access to request.""" diff --git a/lib/tool_shed/context.py b/lib/tool_shed/context.py index 6991bbd112cd..107be8c8ed48 100644 --- a/lib/tool_shed/context.py +++ b/lib/tool_shed/context.py @@ -84,6 +84,10 @@ class SessionRequestContext(ProvidesRepositoriesContext, Protocol): def get_galaxy_session(self) -> Optional[GalaxySession]: ... + @abc.abstractmethod + def set_galaxy_session(self, galaxy_session: GalaxySession): + ... + @abc.abstractproperty def request(self) -> GalaxyAbstractRequest: ... @@ -96,6 +100,10 @@ def response(self) -> GalaxyAbstractResponse: def url_builder(self): ... + @abc.abstractproperty + def session_csrf_token(self) -> str: + ... + class SessionRequestContextImpl(SessionRequestContext): _app: ToolShedApp @@ -133,6 +141,11 @@ def user(self) -> Optional[User]: def get_galaxy_session(self) -> Optional[GalaxySession]: return self._galaxy_session + def set_galaxy_session(self, galaxy_session: GalaxySession): + self._galaxy_session = galaxy_session + if galaxy_session.user: + self._user = galaxy_session.user + @property def repositories_hostname(self) -> str: return str(self.request.base).rstrip("/") @@ -148,3 +161,18 @@ def request(self) -> GalaxyAbstractRequest: @property def response(self) -> GalaxyAbstractResponse: return self.__response + + # Following three things added v2.0 frontend + @property + def session_csrf_token(self): + token = "" + if self._galaxy_session: + token = self.security.encode_id(self._galaxy_session.id, kind="csrf") + return token + + @property + def galaxy_session(self) -> Optional[GalaxySession]: + return self._galaxy_session + + def log_event(self, str): + pass diff --git a/lib/tool_shed/managers/repositories.py b/lib/tool_shed/managers/repositories.py index f72e61fa1a31..805f716164c5 100644 --- a/lib/tool_shed/managers/repositories.py +++ b/lib/tool_shed/managers/repositories.py @@ -433,6 +433,7 @@ def get_repository_metadata_dict(app: ToolShedApp, id: str, recursive: bool, dow metadata_dict["repository_dependencies"] = [] if metadata.includes_tools: metadata_dict["tools"] = metadata.metadata["tools"] + metadata_dict["invalid_tools"] = metadata.metadata.get("invalid_tools", []) all_metadata[f"{int(changeset)}:{changehash}"] = metadata_dict return all_metadata diff --git a/lib/tool_shed/test/base/playwrightbrowser.py b/lib/tool_shed/test/base/playwrightbrowser.py index 6d00f794c69b..d29529cece24 100644 --- a/lib/tool_shed/test/base/playwrightbrowser.py +++ b/lib/tool_shed/test/base/playwrightbrowser.py @@ -13,6 +13,13 @@ ) +class Locators: + toolbar_login = ".toolbar-login" + toolbar_logout = ".toolbar-logout" + login_submit_button = '[name="login_button"]' + register_link = ".register-link" + + class PlaywrightShedBrowser(ShedBrowser): _page: Page @@ -151,3 +158,17 @@ def grant_users_access(self, usernames: List[str]): @property def is_twill(self) -> bool: return False + + def logout_if_logged_in(self, assert_logged_out=True): + self._page.wait_for_selector(f"{Locators.toolbar_login}, {Locators.toolbar_logout}") + logout_locator = self._page.locator(Locators.toolbar_logout) + if logout_locator.is_visible(): + logout_locator.click() + if assert_logged_out: + self.expect_not_logged_in() + + def expect_not_logged_in(self): + expect(self._page.locator(Locators.toolbar_logout)).not_to_be_visible() + + def expect_logged_in(self): + expect(self._page.locator(Locators.toolbar_logout)).to_be_visible() diff --git a/lib/tool_shed/test/base/twilltestcase.py b/lib/tool_shed/test/base/twilltestcase.py index 5eca1b0543f2..8c15af3dc210 100644 --- a/lib/tool_shed/test/base/twilltestcase.py +++ b/lib/tool_shed/test/base/twilltestcase.py @@ -29,6 +29,7 @@ hg, ui, ) +from playwright.sync_api import Page from sqlalchemy import ( and_, false, @@ -74,6 +75,7 @@ ) from .api import ShedApiTestCase from .browser import ShedBrowser +from .playwrightbrowser import PlaywrightShedBrowser from .twillbrowser import ( page_content, visit_url, @@ -692,39 +694,52 @@ def check_string_not_in_page(self, patt): self._browser.check_string_not_in_page(patt) # Functions associated with user accounts + def _submit_register_form(self, email: str, password: str, username: str, redirect: Optional[str] = None): + self._browser.fill_form_value("registration", "email", email) + if redirect is not None: + self._browser.fill_form_value("registration", "redirect", redirect) + self._browser.fill_form_value("registration", "password", password) + self._browser.fill_form_value("registration", "confirm", password) + self._browser.fill_form_value("registration", "username", username) + self._browser.submit_form_with_name("registration", "create_user_button") + + @property + def invalid_tools_labels(self) -> str: + return "Invalid Tools" if self.is_v2 else "Invalid tools" def create(self, cntrller="user", email="test@bx.psu.edu", password="testuser", username="admin-user", redirect=""): # HACK: don't use panels because late_javascripts() messes up the twill browser and it # can't find form fields (and hence user can't be logged in). params = dict(cntrller=cntrller, use_panels=False) self.visit_url("/user/create", params) - self._browser.fill_form_value("registration", "email", email) - self._browser.fill_form_value("registration", "redirect", redirect) - self._browser.fill_form_value("registration", "password", password) - self._browser.fill_form_value("registration", "confirm", password) - self._browser.fill_form_value("registration", "username", username) - self._browser.submit_form_with_name("registration", "create_user_button") + self._submit_register_form( + email, + password, + username, + redirect, + ) previously_created = False username_taken = False invalid_username = False - try: - self.check_page_for_string("Created new user account") - except AssertionError: + if not self.is_v2: try: - # May have created the account in a previous test run... - self.check_page_for_string(f"User with email '{email}' already exists.") - previously_created = True + self.check_page_for_string("Created new user account") except AssertionError: try: - self.check_page_for_string("Public name is taken; please choose another") - username_taken = True + # May have created the account in a previous test run... + self.check_page_for_string(f"User with email '{email}' already exists.") + previously_created = True except AssertionError: - # Note that we're only checking if the usr name is >< 4 chars here... try: - self.check_page_for_string("Public name must be at least 4 characters in length") - invalid_username = True + self.check_page_for_string("Public name is taken; please choose another") + username_taken = True except AssertionError: - pass + # Note that we're only checking if the usr name is >< 4 chars here... + try: + self.check_page_for_string("Public name must be at least 4 characters in length") + invalid_username = True + except AssertionError: + pass return previously_created, username_taken, invalid_username def last_page(self): @@ -748,6 +763,11 @@ def login( redirect: str = "", logout_first: bool = True, ): + if self.is_v2: + # old version had a logout URL, this one needs to check + # page if logged in + self.visit_url("/") + # Clear cookies. if logout_first: self.logout() @@ -755,7 +775,8 @@ def login( previously_created, username_taken, invalid_username = self.create( email=email, password=password, username=username, redirect=redirect ) - if previously_created: + # v2 doesn't log you in on account creation... so force a login here + if previously_created or self.is_v2: # The acount has previously been created, so just login. # HACK: don't use panels because late_javascripts() messes up the twill browser and it # can't find form fields (and hence user can't be logged in). @@ -763,9 +784,27 @@ def login( self.visit_url("/user/login", params=params) self.submit_form(button="login_button", login=email, redirect=redirect, password=password) + @property + def is_v2(self) -> bool: + return self.api_interactor.api_version == "v2" + + @property + def _playwright_browser(self) -> PlaywrightShedBrowser: + # make sure self.is_v2 + browser = self._browser + assert isinstance(browser, PlaywrightShedBrowser) + return browser + + @property + def _page(self) -> Page: + return self._playwright_browser._page + def logout(self): - self.visit_url("/user/logout") - self.check_page_for_string("You have been logged out") + if self.is_v2: + self._playwright_browser.logout_if_logged_in() + else: + self.visit_url("/user/logout") + self.check_page_for_string("You have been logged out") def submit_form(self, form_no=-1, button="runtool_btn", form=None, **kwd): """Populates and submits a form from the keyword arguments.""" @@ -816,12 +855,15 @@ def assign_admin_role(self, repository: Repository, user): self.check_for_strings(strings_displayed=["Role", "has been associated"]) def browse_category(self, category: Category, strings_displayed=None, strings_not_displayed=None): - params = { - "sort": "name", - "operation": "valid_repositories_by_category", - "id": category.id, - } - self.visit_url("/repository/browse_valid_categories", params=params) + if self.is_v2: + self.visit_url(f"/repositories_by_category/{category.id}") + else: + params = { + "sort": "name", + "operation": "valid_repositories_by_category", + "id": category.id, + } + self.visit_url("/repository/browse_valid_categories", params=params) self.check_for_strings(strings_displayed, strings_not_displayed) def browse_repository(self, repository: Repository, strings_displayed=None, strings_not_displayed=None): @@ -835,7 +877,10 @@ def browse_repository_dependencies(self, strings_displayed=None, strings_not_dis self.check_for_strings(strings_displayed, strings_not_displayed) def browse_tool_shed(self, url, strings_displayed=None, strings_not_displayed=None): - url = "/repository/browse_valid_categories" + if self.is_v2: + url = "/repositories_by_category" + else: + url = "/repository/browse_valid_categories" self.visit_url(url) self.check_for_strings(strings_displayed, strings_not_displayed) @@ -875,12 +920,14 @@ def check_repository_changelog(self, repository: Repository, strings_displayed=N def check_repository_dependency( self, repository: Repository, depends_on_repository, depends_on_changeset_revision=None, changeset_revision=None ): - strings_displayed = [depends_on_repository.name, depends_on_repository.owner] - if depends_on_changeset_revision: - strings_displayed.append(depends_on_changeset_revision) - self.display_manage_repository_page( - repository, changeset_revision=changeset_revision, strings_displayed=strings_displayed - ) + if not self.is_v2: + # v2 doesn't display repository repository dependencies, they are deprecated + strings_displayed = [depends_on_repository.name, depends_on_repository.owner] + if depends_on_changeset_revision: + strings_displayed.append(depends_on_changeset_revision) + self.display_manage_repository_page( + repository, changeset_revision=changeset_revision, strings_displayed=strings_displayed + ) def check_repository_metadata(self, repository: Repository, tip_only=True): if tip_only: @@ -1176,7 +1223,10 @@ def display_manage_repository_page( params = {"id": repository.id} if changeset_revision: params["changeset_revision"] = changeset_revision - self.visit_url("/repository/manage_repository", params=params) + url = "/repository/manage_repository" + if self.is_v2: + url = f"/repositories/{repository.id}" + self.visit_url(url, params=params) self.check_for_strings(strings_displayed, strings_not_displayed) def display_repository_clone_page( @@ -1592,17 +1642,20 @@ def load_citable_url( url += f"/{changeset_revision}" self.visit_url(url) self.check_for_strings(strings_displayed, strings_not_displayed) - # Now load the page that should be displayed inside the iframe and check for strings. - if encoded_repository_id: - params = {"id": encoded_repository_id, "operation": "view_or_manage_repository"} - if changeset_revision: - params["changeset_revision"] = changeset_revision - self.visit_url("/repository/view_repository", params=params) - self.check_for_strings(strings_displayed_in_iframe, strings_not_displayed_in_iframe) - elif encoded_user_id: - params = {"user_id": encoded_user_id, "operation": "repositories_by_user"} - self.visit_url("/repository/browse_repositories", params=params) + if self.is_v2: self.check_for_strings(strings_displayed_in_iframe, strings_not_displayed_in_iframe) + else: + # Now load the page that should be displayed inside the iframe and check for strings. + if encoded_repository_id: + params = {"id": encoded_repository_id, "operation": "view_or_manage_repository"} + if changeset_revision: + params["changeset_revision"] = changeset_revision + self.visit_url("/repository/view_repository", params=params) + self.check_for_strings(strings_displayed_in_iframe, strings_not_displayed_in_iframe) + elif encoded_user_id: + params = {"user_id": encoded_user_id, "operation": "repositories_by_user"} + self.visit_url("/repository/browse_repositories", params=params) + self.check_for_strings(strings_displayed_in_iframe, strings_not_displayed_in_iframe) def load_changeset_in_tool_shed( self, repository_id, changeset_revision, strings_displayed=None, strings_not_displayed=None @@ -1694,9 +1747,13 @@ def repository_is_new(self, repository: Repository) -> bool: return tip_ctx.rev() < 0 def reset_metadata_on_selected_repositories(self, repository_ids): - self.visit_url("/admin/reset_metadata_on_selected_repositories_in_tool_shed") - kwd = dict(repository_ids=repository_ids) - self.submit_form(button="reset_metadata_on_selected_repositories_button", **kwd) + if self.is_v2: + for repository_id in repository_ids: + self.populator.reset_metadata(repository_id) + else: + self.visit_url("/admin/reset_metadata_on_selected_repositories_in_tool_shed") + kwd = dict(repository_ids=repository_ids) + self.submit_form(button="reset_metadata_on_selected_repositories_button", **kwd) def reset_metadata_on_installed_repositories(self, repositories): assert self._installation_client diff --git a/lib/tool_shed/test/functional/test_0000_basic_repository_features.py b/lib/tool_shed/test/functional/test_0000_basic_repository_features.py index bb34b1aca3fb..c34727257c17 100644 --- a/lib/tool_shed/test/functional/test_0000_basic_repository_features.py +++ b/lib/tool_shed/test/functional/test_0000_basic_repository_features.py @@ -1,5 +1,6 @@ import logging +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -21,6 +22,9 @@ def test_0000_initiate_users(self): self.login(email=common.test_user_2_email, username=common.test_user_2_name) self.login(email=common.admin_email, username=common.admin_username) + @skip_if_api_v2 + # no replicating the functionality in tool shed 2.0, use Planemo + # to create repositories. def test_0005_create_repository_without_categories(self): """Verify that a repository cannot be created unless at least one category has been defined.""" strings_displayed = ["No categories have been configured in this instance of the Galaxy Tool Shed"] @@ -69,6 +73,7 @@ def test_0025_change_repository_category(self): categories_to_remove=["Test 0000 Basic Repository Features 1"], ) + @skip_if_api_v2 def test_0030_grant_write_access(self): """Grant write access to another user""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) @@ -120,6 +125,7 @@ def test_0040_verify_repository(self): strings_displayed=strings, ) + @skip_if_api_v2 def test_0045_alter_repository_states(self): """Test toggling the malicious and deprecated repository flags.""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) @@ -147,13 +153,14 @@ def test_0045_alter_repository_states(self): strings_displayed = ["Mark repository as deprecated", "Reset all repository metadata"] self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + @skip_if_api_v2 + # probably not porting this functionality - just test + # with Twill for older UI and drop when that is all dropped def test_0050_display_repository_tip_file(self): """Display the contents of filtering.xml in the repository tip revision""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) assert repository if self._browser.is_twill: - # probably not porting this functionality - just test - # with Twill for older UI and drop when that is all dropped self.display_repository_file_contents( repository=repository, filename="filtering.xml", @@ -167,9 +174,7 @@ def test_0055_upload_filtering_txt_file(self): repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) self.add_file_to_repository(repository, "filtering/filtering_0000.txt") expected = self._escape_page_content_if_needed("Readme file for filtering 1.1.0") - self.display_manage_repository_page( - repository, strings_displayed=[expected] - ) + self.display_manage_repository_page(repository, strings_displayed=[expected]) def test_0060_upload_filtering_test_data(self): """Upload filtering test data.""" @@ -197,7 +202,10 @@ def test_0070_verify_filtering_repository(self): repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) tip = self.get_repository_tip(repository) self.check_for_valid_tools(repository) - strings_displayed = ["Select a revision"] + if self.is_v2: + strings_displayed = [] + else: + strings_displayed = ["Select a revision"] self.display_manage_repository_page(repository, strings_displayed=strings_displayed) self.check_count_of_metadata_revisions_associated_with_repository(repository, metadata_count=2) tool_guid = f"{self.url.replace('http://', '').rstrip('/')}/repos/user1/filtering_0000/Filter1/2.2.0" @@ -222,9 +230,7 @@ def test_0075_upload_readme_txt_file(self): repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) self.add_file_to_repository(repository, "readme.txt") content = self._escape_page_content_if_needed("This is a readme file.") - self.display_manage_repository_page( - repository, strings_displayed=[content] - ) + self.display_manage_repository_page(repository, strings_displayed=[content]) # Verify that there is a different readme file for each metadata revision. readme_content = self._escape_page_content_if_needed("Readme file for filtering 1.1.0") self.display_manage_repository_page( @@ -241,10 +247,9 @@ def test_0080_delete_readme_txt_file(self): self.delete_files_from_repository(repository, filenames=["readme.txt"]) self.check_count_of_metadata_revisions_associated_with_repository(repository, metadata_count=2) readme_content = self._escape_page_content_if_needed("Readme file for filtering 1.1.0") - self.display_manage_repository_page( - repository, strings_displayed=[readme_content] - ) + self.display_manage_repository_page(repository, strings_displayed=[readme_content]) + @skip_if_api_v2 # not re-implemented in the UI, there are API tests though def test_0085_search_for_valid_filter_tool(self): """Search for the filtering tool by tool ID, name, and version.""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) @@ -279,16 +284,20 @@ def test_0100_verify_reserved_username_handling(self): self.login(email="baduser@bx.psu.edu", username="repos") test_user_1 = self.test_db_util.get_user("baduser@bx.psu.edu") assert test_user_1 is None, 'Creating user with public name "repos" succeeded.' - error_message = ( - "The term 'repos' is a reserved word in the Tool Shed, so it cannot be used as a public user name." - ) - self.check_for_strings(strings_displayed=[error_message]) + if not self.is_v2: + # no longer use this terminology but the above test case ensures + # the important thing and caught a bug in v2 + error_message = ( + "The term 'repos' is a reserved word in the Tool Shed, so it cannot be used as a public user name." + ) + self.check_for_strings(strings_displayed=[error_message]) def test_0105_contact_repository_owner(self): """""" # We no longer implement this. pass + @skip_if_api_v2 # v2 doesn't implement repository deleting repositories def test_0110_delete_filtering_repository(self): """Delete the filtering_0000 repository and verify that it no longer has any downloadable revisions.""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) @@ -303,6 +312,7 @@ def test_0110_delete_filtering_repository(self): # Marking a repository as deleted should result in no metadata revisions being downloadable. # assert True not in [metadata.downloadable for metadata in self._db_repository(repository).metadata_revisions] + @skip_if_api_v2 # v2 doesn't implement repository deleting repositories def test_0115_undelete_filtering_repository(self): """Undelete the filtering_0000 repository and verify that it now has two downloadable revisions.""" repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) @@ -316,6 +326,7 @@ def test_0115_undelete_filtering_repository(self): assert True in [metadata.downloadable for metadata in self._db_repository(repository).metadata_revisions] assert len(self._db_repository(repository).downloadable_revisions) == 2 + @skip_if_api_v2 # not re-implementing in tool shed 2.0 def test_0120_enable_email_notifications(self): """Enable email notifications for test user 2 on filtering_0000.""" # Log in as test_user_2 @@ -332,9 +343,7 @@ def test_0125_upload_new_readme_file(self): # Upload readme.txt to the filtering_0000 repository and verify that it is now displayed. self.add_file_to_repository(repository, "filtering/readme.txt") content = self._escape_page_content_if_needed("These characters should not") - self.display_manage_repository_page( - repository, strings_displayed=[content] - ) + self.display_manage_repository_page(repository, strings_displayed=[content]) def test_0130_verify_handling_of_invalid_characters(self): """Load the above changeset in the change log and confirm that there is no server error displayed.""" diff --git a/lib/tool_shed/test/functional/test_0010_repository_with_tool_dependencies.py b/lib/tool_shed/test/functional/test_0010_repository_with_tool_dependencies.py index 830e0d0022c7..4b9d27ae19d7 100644 --- a/lib/tool_shed/test/functional/test_0010_repository_with_tool_dependencies.py +++ b/lib/tool_shed/test/functional/test_0010_repository_with_tool_dependencies.py @@ -1,5 +1,6 @@ import os +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -55,8 +56,11 @@ def test_0010_create_freebayes_repository_and_upload_tool_xml(self): assert repository strings_displayed = ["Metadata may have been defined", "This file requires an entry", "tool_data_table_conf"] self.add_file_to_repository(repository, "freebayes/freebayes.xml", strings_displayed=strings_displayed) + if self.is_v2: + # opps... not good right? + self.populator.reset_metadata(repository) self.display_manage_repository_page( - repository, strings_displayed=["Invalid tools"], strings_not_displayed=["Valid tools"] + repository, strings_displayed=[self.invalid_tools_labels], strings_not_displayed=["Valid tools"] ) tip = self.get_repository_tip(repository) strings_displayed = ["requires an entry", "tool_data_table_conf.xml"] @@ -74,7 +78,7 @@ def test_0015_upload_missing_tool_data_table_conf_file(self): repository, "freebayes/tool_data_table_conf.xml.sample", strings_displayed=strings_displayed ) self.display_manage_repository_page( - repository, strings_displayed=["Invalid tools"], strings_not_displayed=["Valid tools"] + repository, strings_displayed=[self.invalid_tools_labels], strings_not_displayed=["Valid tools"] ) tip = self.get_repository_tip(repository) strings_displayed = ["refers to a file", "sam_fa_indices.loc"] @@ -124,6 +128,7 @@ def test_0035_upload_valid_tool_dependency_xml(self): target = os.path.join("freebayes", "tool_dependencies.xml") self.add_file_to_repository(repository, target) + @skip_if_api_v2 def test_0040_verify_tool_dependencies(self): """Verify that the uploaded tool_dependencies.xml specifies the correct package versions. @@ -132,7 +137,7 @@ def test_0040_verify_tool_dependencies(self): """ repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name) strings_displayed = ["freebayes", "0.9.4_9696d0ce8a9", "samtools", "0.1.18", "Valid tools", "package"] - strings_not_displayed = ["Invalid tools"] + strings_not_displayed = [self.invalid_tools_labels] self.display_manage_repository_page( repository, strings_displayed=strings_displayed, strings_not_displayed=strings_not_displayed ) diff --git a/lib/tool_shed/test/functional/test_0020_basic_repository_dependencies.py b/lib/tool_shed/test/functional/test_0020_basic_repository_dependencies.py index 47fe9b0a0b84..f4fce544de59 100644 --- a/lib/tool_shed/test/functional/test_0020_basic_repository_dependencies.py +++ b/lib/tool_shed/test/functional/test_0020_basic_repository_dependencies.py @@ -1,3 +1,4 @@ +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -70,6 +71,7 @@ def test_0025_generate_and_upload_repository_dependencies_xml(self): repository=repository, repository_tuples=[repository_tuple], filepath=repository_dependencies_path ) + @skip_if_api_v2 def test_0030_verify_emboss_5_dependencies(self): """Verify that the emboss_5 repository now depends on the emboss_datatypes repository with correct name, owner, and changeset revision.""" repository = self._get_repository_by_name_and_owner(emboss_repository_name, common.test_user_1_name) diff --git a/lib/tool_shed/test/functional/test_0030_repository_dependency_revisions.py b/lib/tool_shed/test/functional/test_0030_repository_dependency_revisions.py index 3b9883eb439e..8436d84d8794 100644 --- a/lib/tool_shed/test/functional/test_0030_repository_dependency_revisions.py +++ b/lib/tool_shed/test/functional/test_0030_repository_dependency_revisions.py @@ -1,3 +1,4 @@ +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -161,6 +162,7 @@ def test_0045_generate_repository_dependency_on_emboss_6(self): repository=emboss_repository, repository_tuples=[emboss_tuple], filepath=repository_dependencies_path ) + @skip_if_api_v2 def test_0050_verify_repository_dependency_revisions(self): """Verify that different metadata revisions of the emboss repository have different repository dependencies.""" repository = self._get_repository_by_name_and_owner(emboss_repository_name, common.test_user_1_name) diff --git a/lib/tool_shed/test/functional/test_0040_repository_circular_dependencies.py b/lib/tool_shed/test/functional/test_0040_repository_circular_dependencies.py index d0382b3de0cc..c1a5f0de3315 100644 --- a/lib/tool_shed/test/functional/test_0040_repository_circular_dependencies.py +++ b/lib/tool_shed/test/functional/test_0040_repository_circular_dependencies.py @@ -1,3 +1,4 @@ +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -129,6 +130,7 @@ def test_0035_verify_repository_metadata(self): for repository in [freebayes_repository, filtering_repository]: self.verify_unchanged_repository_metadata(repository) + @skip_if_api_v2 def test_0040_verify_tool_dependencies(self): """Verify that freebayes displays tool dependencies.""" repository = self._get_repository_by_name_and_owner(freebayes_repository_name, common.test_user_1_name) diff --git a/lib/tool_shed/test/functional/test_0050_circular_dependencies_4_levels.py b/lib/tool_shed/test/functional/test_0050_circular_dependencies_4_levels.py index bbddfa0c219e..c774cf278cf3 100644 --- a/lib/tool_shed/test/functional/test_0050_circular_dependencies_4_levels.py +++ b/lib/tool_shed/test/functional/test_0050_circular_dependencies_4_levels.py @@ -1,3 +1,4 @@ +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -245,9 +246,11 @@ def test_0045_verify_repository_dependencies(self): self.check_repository_dependency(filtering_repository, emboss_repository) for repository in [bismark_repository, emboss_repository, column_repository]: self.check_repository_dependency(freebayes_repository, repository) - strings_displayed = ["freebayes_0050 depends on freebayes_0050, emboss_0050, column_maker_0050."] - self.display_manage_repository_page(freebayes_repository, strings_displayed=strings_displayed) + if not self.is_v2: + strings_displayed = ["freebayes_0050 depends on freebayes_0050, emboss_0050, column_maker_0050."] + self.display_manage_repository_page(freebayes_repository, strings_displayed=strings_displayed) + @skip_if_api_v2 def test_0050_verify_tool_dependencies(self): """Check that freebayes and emboss display tool dependencies.""" freebayes_repository = self._get_repository_by_name_and_owner( diff --git a/lib/tool_shed/test/functional/test_0070_invalid_tool.py b/lib/tool_shed/test/functional/test_0070_invalid_tool.py index 9462bdb3b251..df577f7ea4d1 100644 --- a/lib/tool_shed/test/functional/test_0070_invalid_tool.py +++ b/lib/tool_shed/test/functional/test_0070_invalid_tool.py @@ -32,7 +32,7 @@ def test_0005_create_category_and_repository(self): ) self.user_populator().setup_bismark_repo(repository) invalid_revision = self.get_repository_first_revision(repository) - self.display_manage_repository_page(repository, strings_displayed=["Invalid tools"]) + self.display_manage_repository_page(repository, strings_displayed=[self.invalid_tools_labels]) valid_revision = self.get_repository_tip(repository) tool_guid = f"{self.url.replace('http://', '').rstrip('/')}/repos/user1/bismark_0070/bismark_methylation_extractor/0.7.7.3" tool_metadata_strings_displayed = [ diff --git a/lib/tool_shed/test/functional/test_0100_complex_repository_dependencies.py b/lib/tool_shed/test/functional/test_0100_complex_repository_dependencies.py index 6ff5a4a431ed..fb7625860558 100644 --- a/lib/tool_shed/test/functional/test_0100_complex_repository_dependencies.py +++ b/lib/tool_shed/test/functional/test_0100_complex_repository_dependencies.py @@ -1,6 +1,7 @@ import logging import os +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -44,10 +45,11 @@ def test_0005_create_bwa_package_repository(self): strings_displayed=[], ) self.add_file_to_repository(repository, "bwa/complex/tool_dependencies.xml") - # Visit the manage repository page for package_bwa_0_5_9_0100. - self.display_manage_repository_page( - repository, strings_displayed=["Tool dependencies", "will not be", "to this repository"] - ) + if not self.is_v2: + # Visit the manage repository page for package_bwa_0_5_9_0100. + self.display_manage_repository_page( + repository, strings_displayed=["Tool dependencies", "will not be", "to this repository"] + ) def test_0010_create_bwa_base_repository(self): """Create and populate bwa_base_0100.""" @@ -183,10 +185,12 @@ def test_0035_generate_complex_repository_dependency(self): version="0.5.9", ) self.check_repository_dependency(base_repository, depends_on_repository=tool_repository) - self.display_manage_repository_page( - base_repository, strings_displayed=["bwa", "0.5.9", "package", changeset_revision] - ) + if not self.is_v2: + self.display_manage_repository_page( + base_repository, strings_displayed=["bwa", "0.5.9", "package", changeset_revision] + ) + @skip_if_api_v2 def test_0040_generate_tool_dependency(self): """Generate and upload a new tool_dependencies.xml file that specifies an arbitrary file on the filesystem, and verify that bwa_base depends on the new changeset revision.""" # The base_repository named bwa_base_repository_0100 is the dependent repository. diff --git a/lib/tool_shed/test/functional/test_0120_simple_repository_dependency_multiple_owners.py b/lib/tool_shed/test/functional/test_0120_simple_repository_dependency_multiple_owners.py index 71faea2f7758..71cd3322379f 100644 --- a/lib/tool_shed/test/functional/test_0120_simple_repository_dependency_multiple_owners.py +++ b/lib/tool_shed/test/functional/test_0120_simple_repository_dependency_multiple_owners.py @@ -65,16 +65,8 @@ def test_0010_verify_datatypes_repository(self): the datatypes that are defined in datatypes_conf.xml. """ repository = self._get_repository_by_name_and_owner(datatypes_repository_name, common.test_user_2_name) - strings_displayed = [ - "BlastXml", - "BlastNucDb", - "BlastProtDb", - "application/xml", - "text/html", - "blastxml", - "blastdbn", - "blastdbp", - ] + # v2 rightfully doesn't display anything about datatypes... + strings_displayed = ["Galaxy datatypes for the BLAST top hit"] self.display_manage_repository_page(repository, strings_displayed=strings_displayed) def test_0015_create_tool_repository(self): @@ -108,7 +100,9 @@ def test_0020_verify_tool_repository(self): """ repository = self._get_repository_by_name_and_owner(tool_repository_name, common.test_user_1_name) strings_displayed = ["blastxml_to_top_descr_0120", "BLAST top hit descriptions", "Make a table from BLAST XML"] - strings_displayed.extend(["0.0.1", "Valid tools"]) + strings_displayed.append("0.0.1") + if not self.is_v2: + strings_displayed.append("Valid tools") self.display_manage_repository_page(repository, strings_displayed=strings_displayed) def test_0025_create_repository_dependency(self): diff --git a/lib/tool_shed/test/functional/test_0140_tool_help_images.py b/lib/tool_shed/test/functional/test_0140_tool_help_images.py index bce376d06a6c..1a8247747700 100644 --- a/lib/tool_shed/test/functional/test_0140_tool_help_images.py +++ b/lib/tool_shed/test/functional/test_0140_tool_help_images.py @@ -1,5 +1,6 @@ import logging +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -56,6 +57,7 @@ def test_0005_create_htseq_count_repository(self): commit_message="Uploaded htseq_count.tar.", ) + @skip_if_api_v2 def test_0010_load_tool_page(self): """Load the tool page and check for the image. diff --git a/lib/tool_shed/test/functional/test_0170_complex_prior_installation_required.py b/lib/tool_shed/test/functional/test_0170_complex_prior_installation_required.py index 6b2b78107609..d0d1b3b1bad4 100644 --- a/lib/tool_shed/test/functional/test_0170_complex_prior_installation_required.py +++ b/lib/tool_shed/test/functional/test_0170_complex_prior_installation_required.py @@ -125,6 +125,7 @@ def test_0020_verify_generated_dependency(self): ) changeset_revision = self.get_repository_tip(numpy_repository) self.check_repository_dependency(matplotlib_repository, depends_on_repository=numpy_repository) - self.display_manage_repository_page( - matplotlib_repository, strings_displayed=["numpy", "1.7", "package", changeset_revision] - ) + if not self.is_v2: + self.display_manage_repository_page( + matplotlib_repository, strings_displayed=["numpy", "1.7", "package", changeset_revision] + ) diff --git a/lib/tool_shed/test/functional/test_0420_citable_urls_for_repositories.py b/lib/tool_shed/test/functional/test_0420_citable_urls_for_repositories.py index 31d06c129fe2..0a46eec3fe7b 100644 --- a/lib/tool_shed/test/functional/test_0420_citable_urls_for_repositories.py +++ b/lib/tool_shed/test/functional/test_0420_citable_urls_for_repositories.py @@ -9,7 +9,7 @@ repository_name = "filtering_0420" repository_description = "Galaxy filtering tool for test 0420" -repository_long_description = "Long description of Galaxy filtering tool for test 0410" +repository_long_description = "Long description of Galaxy filtering tool for test 0420" first_changeset_hash = "" @@ -88,9 +88,12 @@ def test_0015_load_user_view_page(self): # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, # then directly load the url that the iframe should be loading and check for the expected strings. # The iframe should point to /repository/browse_repositories?user_id=&operation=repositories_by_user - strings_displayed = ["/repository/browse_repositories", encoded_user_id, "operation=repositories_by_user"] - strings_displayed.append(encoded_user_id) - strings_displayed_in_iframe = ["user1", "filtering_0420", "Galaxy filtering tool for test 0420"] + if self.is_v2: + strings_displayed = [] + else: + strings_displayed = ["/repository/browse_repositories", encoded_user_id, "operation=repositories_by_user"] + strings_displayed.append(encoded_user_id) + strings_displayed_in_iframe = ["user1", "filtering_0420", repository_description] self.load_citable_url( username="user1", repository_name=None, @@ -115,11 +118,19 @@ def test_0020_load_repository_view_page(self): # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, # then directly load the url that the iframe should be loading and check for the expected strings. # The iframe should point to /repository/bview_repository?id= - strings_displayed = ["/repository", "view_repository", "id=", encoded_repository_id] - strings_displayed_in_iframe = ["user1", "filtering_0420", "Galaxy filtering tool for test 0420"] + if self.is_v2: + strings_displayed = [] + else: + strings_displayed = ["/repository", "view_repository", "id=", encoded_repository_id] + strings_displayed_in_iframe = [ + "user1", + "filtering_0420", + self._escape_page_content_if_needed(repository_long_description), + ] strings_displayed_in_iframe.append(self.get_repository_tip(repository)) - strings_displayed_in_iframe.append("Link to this repository:") - strings_displayed_in_iframe.append(f"{self.url}/view/user1/filtering_0420") + if not self.is_v2: + strings_displayed_in_iframe.append("Link to this repository:") + strings_displayed_in_iframe.append(f"{self.url}/view/user1/filtering_0420") self.load_citable_url( username="user1", repository_name="filtering_0420", @@ -145,15 +156,19 @@ def test_0025_load_view_page_for_previous_revision(self): # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, # then directly load the url that the iframe should be loading and check for the expected strings. # The iframe should point to /repository/view_repository?id= - strings_displayed = ["/repository", "view_repository", f"id={encoded_repository_id}"] + if self.is_v2: + strings_displayed = [] + else: + strings_displayed = ["/repository", "view_repository", f"id={encoded_repository_id}"] strings_displayed_in_iframe = [ "user1", "filtering_0420", - "Galaxy filtering tool for test 0420", + self._escape_page_content_if_needed(repository_long_description), first_changeset_hash, ] - strings_displayed_in_iframe.append("Link to this repository revision:") - strings_displayed_in_iframe.append(f"{self.url}/view/user1/filtering_0420/{first_changeset_hash}") + if not self.is_v2: + strings_displayed_in_iframe.append("Link to this repository revision:") + strings_displayed_in_iframe.append(f"{self.url}/view/user1/filtering_0420/{first_changeset_hash}") strings_not_displayed_in_iframe = [] self.load_citable_url( username="user1", @@ -173,13 +188,16 @@ def test_0030_load_sharable_url_with_invalid_changeset_revision(self): encoded_user_id = self.security.encode_id(test_user_1.id) encoded_repository_id = repository.id invalid_changeset_hash = "invalid" - # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, - # then directly load the url that the iframe should be loading and check for the expected strings. - # The iframe should point to /repository/view_repository?id=&status=error - strings_displayed = ["/repository", "view_repository", f"id={encoded_repository_id}"] - strings_displayed.extend( - ["The+change+log", "does+not+include+revision", invalid_changeset_hash, "status=error"] - ) + if not self.is_v2: + # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, + # then directly load the url that the iframe should be loading and check for the expected strings. + # The iframe should point to /repository/view_repository?id=&status=error + strings_displayed = ["/repository", "view_repository", f"id={encoded_repository_id}"] + strings_displayed.extend( + ["The+change+log", "does+not+include+revision", invalid_changeset_hash, "status=error"] + ) + else: + strings_displayed = ["The change log does not include revision " + invalid_changeset_hash] self.load_citable_url( username="user1", repository_name="filtering_0420", @@ -200,12 +218,16 @@ def test_0035_load_sharable_url_with_invalid_repository_name(self): # Since twill does not load the contents of an iframe, we need to check that the iframe has been generated correctly, # then directly load the url that the iframe should be loading and check for the expected strings. # The iframe should point to /repository/browse_repositories?user_id=&operation=repositories_by_user - strings_displayed = ["/repository", "browse_repositories", "user1"] - strings_displayed.extend( - ["list+of+repositories+owned", "does+not+include+one+named", "%21%21invalid%21%21", "status=error"] - ) - strings_displayed_in_iframe = ["user1", "filtering_0420"] - strings_displayed_in_iframe.append("Repositories Owned by user1") + if not self.is_v2: + strings_displayed = ["/repository", "browse_repositories", "user1"] + strings_displayed.extend( + ["list+of+repositories+owned", "does+not+include+one+named", "%21%21invalid%21%21", "status=error"] + ) + strings_displayed_in_iframe = ["user1", "filtering_0420"] + strings_displayed_in_iframe.append("Repositories Owned by user1") + else: + strings_displayed = ["Repository user1/!!invalid!! is not found"] + strings_displayed_in_iframe = [] self.load_citable_url( username="user1", repository_name="!!invalid!!", @@ -222,7 +244,10 @@ def test_0040_load_sharable_url_with_invalid_owner(self): We are at step 8. Visit the following url and check for appropriate strings: /view/!!invalid!! """ - strings_displayed = ["The tool shed", self.url, "contains no repositories owned by", "!!invalid!!"] + if not self.is_v2: + strings_displayed = ["The tool shed", self.url, "contains no repositories owned by", "!!invalid!!"] + else: + strings_displayed = ["No repositories found"] self.load_citable_url( username="!!invalid!!", repository_name=None, diff --git a/lib/tool_shed/test/functional/test_0430_browse_utilities.py b/lib/tool_shed/test/functional/test_0430_browse_utilities.py index 104d2e0e28b3..0202c5baf2ca 100644 --- a/lib/tool_shed/test/functional/test_0430_browse_utilities.py +++ b/lib/tool_shed/test/functional/test_0430_browse_utilities.py @@ -1,5 +1,6 @@ import logging +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -85,6 +86,7 @@ def test_0020_create_tool_dependency_repository(self): commit_message="Uploaded freebayes.tar.", ) + @skip_if_api_v2 def test_0030_browse_tools(self): """Load the page to browse tools. @@ -96,6 +98,7 @@ def test_0030_browse_tools(self): strings_displayed = ["EMBOSS", "antigenic1", "5.0.0", changeset_revision, "user1", "emboss_0430"] self.browse_tools(strings_displayed=strings_displayed) + @skip_if_api_v2 def test_0040_browse_tool_dependencies(self): """Browse tool dependencies and look for the right versions of freebayes and samtools. diff --git a/lib/tool_shed/test/functional/test_0460_upload_to_repository.py b/lib/tool_shed/test/functional/test_0460_upload_to_repository.py index 5c8cb5ea9907..d1c04840ef8e 100644 --- a/lib/tool_shed/test/functional/test_0460_upload_to_repository.py +++ b/lib/tool_shed/test/functional/test_0460_upload_to_repository.py @@ -129,12 +129,13 @@ def test_0020_populate_complex_dependency_test_1_0460(self): repository = self._get_repository_by_name_and_owner("complex_dependency_test_1_0460", common.test_user_1_name) package_repository = self._get_repository_by_name_and_owner("package_bwa_0_5_9_0460", common.test_user_1_name) self.add_file_to_repository(repository, "0460_files/tool_dependencies.xml") - changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, filename="tool_dependencies.xml", strings_displayed=[changeset_revision] - ) + if not self.is_v2: + changeset_revision = self.get_repository_tip(package_repository) + strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, filename="tool_dependencies.xml", strings_displayed=[changeset_revision] + ) def test_0025_populate_complex_dependency_test_2_0460(self): """Populate complex_dependency_test_2_0460. @@ -149,12 +150,13 @@ def test_0025_populate_complex_dependency_test_2_0460(self): "0460_files/tool_dependencies_in_root.tar", commit_message="Uploaded complex repository dependency definition.", ) - changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, filename="tool_dependencies.xml", strings_displayed=[changeset_revision] - ) + if not self.is_v2: + changeset_revision = self.get_repository_tip(package_repository) + strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, filename="tool_dependencies.xml", strings_displayed=[changeset_revision] + ) def test_0030_populate_complex_dependency_test_3_0460(self): """Populate complex_dependency_test_3_0460. @@ -170,11 +172,15 @@ def test_0030_populate_complex_dependency_test_3_0460(self): commit_message="Uploaded complex repository dependency definition.", ) changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, filename="tool_dependencies.xml", filepath="subfolder", strings_displayed=[changeset_revision] - ) + if not self.is_v2: + strings_displayed = ["package_bwa_0_5_9_0460", "bwa", "0.5.9", "package", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, + filename="tool_dependencies.xml", + filepath="subfolder", + strings_displayed=[changeset_revision], + ) def test_0035_create_repositories_for_url_upload(self): """Create and populate hg_tool_dependency_0460 and hg_subfolder_tool_dependency_0460. @@ -244,11 +250,12 @@ def test_0055_populate_repository_dependency_test_1_0460(self): package_repository = self._get_repository_by_name_and_owner(bwa_repository_name, common.test_user_1_name) self.add_file_to_repository(repository, "0460_files/repository_dependencies.xml") changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = [bwa_repository_name, "user1", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, filename="repository_dependencies.xml", strings_displayed=[changeset_revision] - ) + if not self.is_v2: + strings_displayed = [bwa_repository_name, "user1", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, filename="repository_dependencies.xml", strings_displayed=[changeset_revision] + ) def test_0060_populate_repository_dependency_test_2_0460(self): """Populate repository_dependency_test_2_0460. @@ -265,11 +272,12 @@ def test_0060_populate_repository_dependency_test_2_0460(self): commit_message="Uploaded complex repository dependency definition.", ) changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = [bwa_repository_name, "user1", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, filename="repository_dependencies.xml", strings_displayed=[changeset_revision] - ) + if not self.is_v2: + strings_displayed = [bwa_repository_name, "user1", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, filename="repository_dependencies.xml", strings_displayed=[changeset_revision] + ) def test_0065_populate_repository_dependency_test_3_0460(self): """Populate repository_dependency_test_3_0460. @@ -287,14 +295,15 @@ def test_0065_populate_repository_dependency_test_3_0460(self): commit_message="Uploaded complex repository dependency definition.", ) changeset_revision = self.get_repository_tip(package_repository) - strings_displayed = [bwa_repository_name, "user1", changeset_revision] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) - self.display_repository_file_contents( - repository, - filename="repository_dependencies.xml", - filepath="subfolder", - strings_displayed=[changeset_revision], - ) + if not self.is_v2: + strings_displayed = [bwa_repository_name, "user1", changeset_revision] + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + self.display_repository_file_contents( + repository, + filename="repository_dependencies.xml", + filepath="subfolder", + strings_displayed=[changeset_revision], + ) def test_0070_create_repositories_for_url_upload(self): """Create and populate hg_repository_dependency_0460 and hg_subfolder_repository_dependency_0460. diff --git a/lib/tool_shed/test/functional/test_0530_repository_admin_feature.py b/lib/tool_shed/test/functional/test_0530_repository_admin_feature.py index 19294287a283..348f70281e88 100644 --- a/lib/tool_shed/test/functional/test_0530_repository_admin_feature.py +++ b/lib/tool_shed/test/functional/test_0530_repository_admin_feature.py @@ -1,5 +1,6 @@ import logging +from ..base.api import skip_if_api_v2 from ..base.twilltestcase import ( common, ShedTwillTestCase, @@ -98,6 +99,7 @@ def test_0020_rename_repository(self): repository = self._get_repository_by_name_and_owner("renamed_filtering_0530", common.test_user_1_name) assert repository.name == "renamed_filtering_0530", "Repository was not renamed to renamed_filtering_0530." + @skip_if_api_v2 def test_0030_verify_access_denied(self): """Make sure a non-admin user can't modify the repository. diff --git a/lib/tool_shed/test/functional/test_0550_metadata_updated_dependencies.py b/lib/tool_shed/test/functional/test_0550_metadata_updated_dependencies.py index f622b05ac2ab..8c5b08cec3f9 100644 --- a/lib/tool_shed/test/functional/test_0550_metadata_updated_dependencies.py +++ b/lib/tool_shed/test/functional/test_0550_metadata_updated_dependencies.py @@ -71,10 +71,11 @@ def test_0005_freebayes_repository(self): freebayes, "0550_files/package_freebayes_1_0550.tgz", ) - # Visit the manage repository page for package_freebayes_0_5_9_0100. - self.display_manage_repository_page( - freebayes, strings_displayed=["Tool dependencies", "will not be", "to this repository"] - ) + if not self.is_v2: + # Visit the manage repository page for package_freebayes_0_5_9_0100. + self.display_manage_repository_page( + freebayes, strings_displayed=["Tool dependencies", "will not be", "to this repository"] + ) def test_0010_create_samtools_repository(self): """Create and populate the package_samtools_0550 repository.""" @@ -118,7 +119,8 @@ def test_0020_check_repository_dependency(self): samtools = self._get_repository_by_name_and_owner(repositories["samtools"]["name"], common.test_user_1_name) filtering = self._get_repository_by_name_and_owner(repositories["filtering"]["name"], common.test_user_1_name) strings_displayed = [freebayes.id, samtools.id] - self.display_manage_repository_page(filtering, strings_displayed=strings_displayed) + if not self.is_v2: + self.display_manage_repository_page(filtering, strings_displayed=strings_displayed) def test_0025_update_dependent_repositories(self): """ diff --git a/lib/tool_shed/test/functional/test_1010_install_repository_with_tool_dependencies.py b/lib/tool_shed/test/functional/test_1010_install_repository_with_tool_dependencies.py index 2df6669aee2f..976d2c354ecc 100644 --- a/lib/tool_shed/test/functional/test_1010_install_repository_with_tool_dependencies.py +++ b/lib/tool_shed/test/functional/test_1010_install_repository_with_tool_dependencies.py @@ -43,10 +43,11 @@ def test_0010_browse_tool_shed(self): self.browse_tool_shed(url=self.url, strings_displayed=[category_name]) category = self.populator.get_category_with_name(category_name) self.browse_category(category, strings_displayed=[repository_name]) - strings_displayed = [repository_name, "Valid tools", "Tool dependencies"] - self.preview_repository_in_tool_shed( - repository_name, common.test_user_1_name, strings_displayed=strings_displayed - ) + if not self.is_v2: + strings_displayed = [repository_name, "Valid tools", "Tool dependencies"] + self.preview_repository_in_tool_shed( + repository_name, common.test_user_1_name, strings_displayed=strings_displayed + ) def test_0015_install_freebayes_repository(self): """Install the freebayes repository without installing tool dependencies.""" diff --git a/lib/tool_shed/test/functional/test_1020_install_repository_with_repository_dependencies.py b/lib/tool_shed/test/functional/test_1020_install_repository_with_repository_dependencies.py index fbcfbb28d3be..b965011e7ca0 100644 --- a/lib/tool_shed/test/functional/test_1020_install_repository_with_repository_dependencies.py +++ b/lib/tool_shed/test/functional/test_1020_install_repository_with_repository_dependencies.py @@ -73,9 +73,12 @@ def test_0010_browse_tool_shed(self): self.browse_tool_shed(url=self.url, strings_displayed=["Test 0020 Basic Repository Dependencies"]) category = self.populator.get_category_with_name("Test 0020 Basic Repository Dependencies") self.browse_category(category, strings_displayed=[emboss_repository_name]) - self.preview_repository_in_tool_shed( - emboss_repository_name, common.test_user_1_name, strings_displayed=[emboss_repository_name, "Valid tools"] - ) + if not self.is_v2: + self.preview_repository_in_tool_shed( + emboss_repository_name, + common.test_user_1_name, + strings_displayed=[emboss_repository_name, "Valid tools"], + ) def test_0015_install_emboss_repository(self): """Install the emboss repository without installing tool dependencies.""" diff --git a/lib/tool_shed/test/functional/test_1030_install_repository_with_dependency_revisions.py b/lib/tool_shed/test/functional/test_1030_install_repository_with_dependency_revisions.py index 17cebc758712..3cc894c038cb 100644 --- a/lib/tool_shed/test/functional/test_1030_install_repository_with_dependency_revisions.py +++ b/lib/tool_shed/test/functional/test_1030_install_repository_with_dependency_revisions.py @@ -140,9 +140,12 @@ def test_0010_browse_tool_shed(self): self.browse_tool_shed(url=self.url, strings_displayed=["Test 0030 Repository Dependency Revisions"]) category = self.populator.get_category_with_name("Test 0030 Repository Dependency Revisions") self.browse_category(category, strings_displayed=[emboss_repository_name]) - self.preview_repository_in_tool_shed( - emboss_repository_name, common.test_user_1_name, strings_displayed=[emboss_repository_name, "Valid tools"] - ) + if not self.is_v2: + self.preview_repository_in_tool_shed( + emboss_repository_name, + common.test_user_1_name, + strings_displayed=[emboss_repository_name, "Valid tools"], + ) def test_0015_install_emboss_repository(self): """Install the emboss repository without installing tool dependencies.""" diff --git a/lib/tool_shed/test/functional/test_1050_circular_dependencies_4_levels.py b/lib/tool_shed/test/functional/test_1050_circular_dependencies_4_levels.py index 48e7039032a3..e57633296faa 100644 --- a/lib/tool_shed/test/functional/test_1050_circular_dependencies_4_levels.py +++ b/lib/tool_shed/test/functional/test_1050_circular_dependencies_4_levels.py @@ -269,7 +269,8 @@ def test_0045_verify_repository_dependencies(self): strings_displayed = [ f"{freebayes_repository.name} depends on {', '.join(repo.name for repo in freebayes_dependencies)}." ] - self.display_manage_repository_page(freebayes_repository, strings_displayed=strings_displayed) + if not self.is_v2: + self.display_manage_repository_page(freebayes_repository, strings_displayed=strings_displayed) def test_0050_verify_tool_dependencies(self): """Check that freebayes and emboss display tool dependencies.""" @@ -277,13 +278,14 @@ def test_0050_verify_tool_dependencies(self): freebayes_repository_name, common.test_user_1_name ) emboss_repository = self._get_repository_by_name_and_owner(emboss_repository_name, common.test_user_1_name) - self.display_manage_repository_page( - freebayes_repository, - strings_displayed=["freebayes", "0.9.4_9696d0ce8a9", "samtools", "0.1.18", "Tool dependencies"], - ) - self.display_manage_repository_page( - emboss_repository, strings_displayed=["Tool dependencies", "emboss", "5.0.0", "package"] - ) + if not self.is_v2: + self.display_manage_repository_page( + freebayes_repository, + strings_displayed=["freebayes", "0.9.4_9696d0ce8a9", "samtools", "0.1.18", "Tool dependencies"], + ) + self.display_manage_repository_page( + emboss_repository, strings_displayed=["Tool dependencies", "emboss", "5.0.0", "package"] + ) def test_0055_install_column_repository(self): """Install column_maker with repository dependencies.""" diff --git a/lib/tool_shed/test/functional/test_1140_simple_repository_dependency_multiple_owners.py b/lib/tool_shed/test/functional/test_1140_simple_repository_dependency_multiple_owners.py index 557f20fa8ed6..12067ac1bce8 100644 --- a/lib/tool_shed/test/functional/test_1140_simple_repository_dependency_multiple_owners.py +++ b/lib/tool_shed/test/functional/test_1140_simple_repository_dependency_multiple_owners.py @@ -84,7 +84,8 @@ def test_0010_verify_datatypes_repository(self): "blastdbn", "blastdbp", ] - self.display_manage_repository_page(repository, strings_displayed=strings_displayed) + if not self.is_v2: + self.display_manage_repository_page(repository, strings_displayed=strings_displayed) def test_0015_create_tool_repository(self): """Create and populate the blastxml_to_top_descr_0120 repository @@ -126,7 +127,7 @@ def test_0020_verify_tool_repository(self): """ repository = self._get_repository_by_name_and_owner(tool_repository_name, common.test_user_1_name) strings_displayed = ["blastxml_to_top_descr_0120", "BLAST top hit descriptions", "Make a table from BLAST XML"] - strings_displayed.extend(["0.0.1", "Valid tools"]) + strings_displayed.extend(["0.0.1"]) self.display_manage_repository_page(repository, strings_displayed=strings_displayed) def test_0025_create_repository_dependency(self): diff --git a/lib/tool_shed/test/functional/test_1160_tool_help_images.py b/lib/tool_shed/test/functional/test_1160_tool_help_images.py index dd46f0dbae92..add92d62dc55 100644 --- a/lib/tool_shed/test/functional/test_1160_tool_help_images.py +++ b/lib/tool_shed/test/functional/test_1160_tool_help_images.py @@ -69,6 +69,8 @@ def test_0010_load_tool_page(self): # should be the tool that contains a link to the image. repository_metadata = self._db_repository(repository).metadata_revisions[0].metadata tool_path = repository_metadata["tools"][0]["tool_config"] - self.load_display_tool_page( - repository, tool_path, changeset_revision, strings_displayed=[image_path], strings_not_displayed=[] - ) + # V2 is not going to have this page right? So... do we need this test at all or that route? Likely not? + if self._browser.is_twill and not self.is_v2: + self.load_display_tool_page( + repository, tool_path, changeset_revision, strings_displayed=[image_path], strings_not_displayed=[] + ) diff --git a/lib/tool_shed/test/functional/test_1190_complex_prior_installation_required.py b/lib/tool_shed/test/functional/test_1190_complex_prior_installation_required.py index 57714179cd0e..b49ae8998ca2 100644 --- a/lib/tool_shed/test/functional/test_1190_complex_prior_installation_required.py +++ b/lib/tool_shed/test/functional/test_1190_complex_prior_installation_required.py @@ -136,9 +136,10 @@ def test_0020_verify_generated_dependency(self): ) changeset_revision = self.get_repository_tip(numpy_repository) self.check_repository_dependency(matplotlib_repository, depends_on_repository=numpy_repository) - self.display_manage_repository_page( - matplotlib_repository, strings_displayed=["numpy", "1.7", "package", changeset_revision] - ) + if not self.is_v2: + self.display_manage_repository_page( + matplotlib_repository, strings_displayed=["numpy", "1.7", "package", changeset_revision] + ) def test_0025_install_matplotlib_repository(self): """Install the package_matplotlib_1_2_0170 repository. diff --git a/lib/tool_shed/test/functional/test_frontend_login.py b/lib/tool_shed/test/functional/test_frontend_login.py new file mode 100644 index 000000000000..1e752c37e934 --- /dev/null +++ b/lib/tool_shed/test/functional/test_frontend_login.py @@ -0,0 +1,76 @@ +from playwright.sync_api import ( + expect, + Page, +) + +from galaxy_test.base.api_util import random_name +from ..base.api import skip_if_api_v1 +from ..base.playwrightbrowser import ( + Locators, + PlaywrightShedBrowser, +) +from ..base.twilltestcase import ShedTwillTestCase + + +class PlaywrightTestCase(ShedTwillTestCase): + @property + def _playwright_browser(self) -> PlaywrightShedBrowser: + browser = self._browser + assert isinstance(browser, PlaywrightShedBrowser) + return browser + + @property + def _page(self) -> Page: + return self._playwright_browser._page + + +TEST_PASSWORD = "testpass" + + +class TestFrontendLogin(PlaywrightTestCase): + @skip_if_api_v1 + def test_register(self): + self.visit_url("/") + page = self._page + expect(page.locator(Locators.toolbar_login)).to_be_visible() + page.click(Locators.toolbar_login) + expect(page.locator(Locators.login_submit_button)).to_be_visible() + expect(page.locator(Locators.register_link)).to_be_visible() + page.click(Locators.register_link) + user = random_name(prefix="shduser") + self._submit_register_form( + f"{user}@galaxyproject.org", + TEST_PASSWORD, + user, + ) + expect(page.locator(Locators.login_submit_button)).to_be_visible() + + @skip_if_api_v1 + def test_create(self): + user = random_name(prefix="shduser") + self.create( + email=f"{user}@galaxyproject.org", + password=TEST_PASSWORD, + username=user, + ) + + @skip_if_api_v1 + def test_logout(self): + self._create_and_login() + self._playwright_browser.expect_logged_in() + self._playwright_browser.logout_if_logged_in() + self._playwright_browser.expect_not_logged_in() + + @skip_if_api_v1 + def test_change_password(self): + self._create_and_login() + + def _create_and_login(self): + user = random_name(prefix="shduser") + email = f"{user}@galaxyproject.org" + self.create( + email=email, + password=TEST_PASSWORD, + username=user, + ) + self.login(email, TEST_PASSWORD, username=user, redirect=None) diff --git a/lib/tool_shed/test/functional/test_shed_graphql.py b/lib/tool_shed/test/functional/test_shed_graphql.py new file mode 100644 index 000000000000..c427732872d8 --- /dev/null +++ b/lib/tool_shed/test/functional/test_shed_graphql.py @@ -0,0 +1,21 @@ +from galaxy_test.base.api_asserts import assert_status_code_is_ok +from ..base.api import ( + ShedApiTestCase, + skip_if_api_v1, +) + + +class TestShedGraphqlApi(ShedApiTestCase): + @skip_if_api_v1 + def test_graphql_query(self): + populator = self.populator + category = populator.new_category(prefix="testcreate") + json = {"query": r"query { categories { name } }"} + response = self.api_interactor.post("graphql/", json=json) + assert_status_code_is_ok(response) + result = response.json() + assert "data" in result + data = result["data"] + assert "categories" in data + categories = data["categories"] + assert category.name in [c["name"] for c in categories] diff --git a/lib/tool_shed/test/functional/test_shed_repositories.py b/lib/tool_shed/test/functional/test_shed_repositories.py index 2fd4f551a08e..90e9b8134059 100644 --- a/lib/tool_shed/test/functional/test_shed_repositories.py +++ b/lib/tool_shed/test/functional/test_shed_repositories.py @@ -61,6 +61,14 @@ def test_metadata_simple(self): assert only_revision.downloadable assert not only_revision.malicious + def test_metadata_invalid_tools(self): + populator = self.populator + repository = populator.setup_bismark_repo() + repository_metadata = populator.get_metadata(repository) + assert repository_metadata + for _, value in repository_metadata.__root__.items(): + assert value.invalid_tools + def test_index_simple(self): # Logic and typing is pretty different if given a tool id to search for - this should # be tested or dropped in v2. diff --git a/lib/tool_shed/util/metadata_util.py b/lib/tool_shed/util/metadata_util.py index 47d82928f0b6..c41b62b7a7c0 100644 --- a/lib/tool_shed/util/metadata_util.py +++ b/lib/tool_shed/util/metadata_util.py @@ -45,6 +45,7 @@ def get_all_dependencies(app, metadata_entry, processed_dependency_links=None): dependency_dict["repository"] = repository.to_dict(value_mapper=value_mapper) if dependency_metadata.includes_tools: dependency_dict["tools"] = dependency_metadata.metadata["tools"] + dependency_dict["invalid_tools"] = dependency_metadata.metadata.get("invalid_tools", []) dependency_dict["repository_dependencies"] = [] if dependency_dict["includes_tool_dependencies"]: dependency_dict["tool_dependencies"] = repository.get_tool_dependencies( diff --git a/lib/tool_shed/webapp/api2/__init__.py b/lib/tool_shed/webapp/api2/__init__.py index 7f270db14d4a..6961c8407b93 100644 --- a/lib/tool_shed/webapp/api2/__init__.py +++ b/lib/tool_shed/webapp/api2/__init__.py @@ -1,3 +1,4 @@ +import logging from json import JSONDecodeError from typing import ( AsyncGenerator, @@ -29,7 +30,9 @@ from galaxy.managers.session import GalaxySessionManager from galaxy.managers.users import UserManager from galaxy.security.idencoding import IdEncodingHelper +from galaxy.util import unicodify from galaxy.web.framework.decorators import require_admin_message +from galaxy.webapps.base.webapp import create_new_session from galaxy.webapps.galaxy.api import ( depends as framework_depends, FrameworkRouter, @@ -49,6 +52,8 @@ User, ) +log = logging.getLogger(__name__) + def get_app() -> ToolShedApp: if tool_shed_app_mod.app is None: @@ -67,10 +72,11 @@ async def get_app_with_request_session() -> AsyncGenerator[ToolShedApp, None]: DependsOnApp = cast(ToolShedApp, Depends(get_app_with_request_session)) +AUTH_COOKIE_NAME = "galaxycommunitysession" api_key_query = APIKeyQuery(name="key", auto_error=False) api_key_header = APIKeyHeader(name="x-api-key", auto_error=False) -api_key_cookie = APIKeyCookie(name="galaxycommunitysession", auto_error=False) +api_key_cookie = APIKeyCookie(name=AUTH_COOKIE_NAME, auto_error=False) def depends(dep_type: Type[T]) -> T: @@ -218,7 +224,7 @@ async def get_body(request: Request): DownloadableQueryParam: bool = Query( default=True, title="downloadable_only", - description="Include only downable repositories.", + description="Include only downloadable repositories.", ) CommitMessage: str = Query( @@ -271,3 +277,79 @@ async def get_body(request: Request): CategoryRepositoriesSortKeyQueryParam: str = Query("name", title="Sort Key") CategoryRepositoriesSortOrderQueryParam: str = Query("asc", title="Sort Order") CategoryRepositoriesPageQueryParam: Optional[int] = Query(None, title="Page") + + +def ensure_valid_session(trans: SessionRequestContext) -> None: + """ + Ensure that a valid Galaxy session exists and is available as + trans.session (part of initialization) + """ + app = trans.app + mapping = app.model + session_manager = GalaxySessionManager(mapping) + sa_session = app.model.context + request = trans.request + # Try to load an existing session + secure_id = request.get_cookie(AUTH_COOKIE_NAME) + galaxy_session = None + prev_galaxy_session = None + user_for_new_session = None + invalidate_existing_session = False + # Track whether the session has changed so we can avoid calling flush + # in the most common case (session exists and is valid). + galaxy_session_requires_flush = False + if secure_id: + session_key: Optional[str] = app.security.decode_guid(secure_id) + if session_key: + # We do NOT catch exceptions here, if the database is down the request should fail, + # and we should not generate a new session. + galaxy_session = session_manager.get_session_from_session_key(session_key=session_key) + if not galaxy_session: + session_key = None + + if galaxy_session is not None and galaxy_session.user is not None and galaxy_session.user.deleted: + invalidate_existing_session = True + log.warning(f"User '{galaxy_session.user.email}' is marked deleted, invalidating session") + # Do we need to invalidate the session for some reason? + if invalidate_existing_session: + assert galaxy_session + prev_galaxy_session = galaxy_session + prev_galaxy_session.is_valid = False + galaxy_session = None + # No relevant cookies, or couldn't find, or invalid, so create a new session + if galaxy_session is None: + galaxy_session = create_new_session(trans, prev_galaxy_session, user_for_new_session) + galaxy_session_requires_flush = True + trans.set_galaxy_session(galaxy_session) + set_auth_cookie(trans, galaxy_session) + else: + trans.set_galaxy_session(galaxy_session) + # Do we need to flush the session? + if galaxy_session_requires_flush: + sa_session.add(galaxy_session) + # FIXME: If prev_session is a proper relation this would not + # be needed. + if prev_galaxy_session: + sa_session.add(prev_galaxy_session) + sa_session.flush() + + +def set_auth_cookie(trans: SessionRequestContext, session): + cookie_name = AUTH_COOKIE_NAME + set_cookie(trans, trans.app.security.encode_guid(session.session_key), cookie_name) + + +def set_cookie(trans: SessionRequestContext, value: str, key, path="/", age=90) -> None: + """Convenience method for setting a session cookie""" + # In wsgi we were setting both a max_age and and expires, but + # all browsers support max_age now. + domain: Optional[str] = trans.app.config.cookie_domain + trans.response.set_cookie( + key, + unicodify(value), + path=path, + max_age=3600 * 24 * age, # 90 days + httponly=True, + secure=trans.request.is_secure, + domain=domain, + ) diff --git a/lib/tool_shed/webapp/api2/repositories.py b/lib/tool_shed/webapp/api2/repositories.py index 9cbf95c91913..776e4064a7ab 100644 --- a/lib/tool_shed/webapp/api2/repositories.py +++ b/lib/tool_shed/webapp/api2/repositories.py @@ -190,6 +190,21 @@ def metadata( return as_dict # return _hack_fastapi_4428(as_dict) + @router.get( + "/api_internal/repositories/{encoded_repository_id}/metadata", + description="Get information about repository metadata", + operation_id="repositories__internal_metadata", + response_model=RepositoryMetadata, + ) + def metadata_internal( + self, + encoded_repository_id: str = RepositoryIdPathParam, + downloadable_only: bool = DownloadableQueryParam, + ) -> dict: + recursive = True + as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only) + return _hack_fastapi_4428(as_dict) + @router.get( "/api/repositories/get_ordered_installable_revisions", description="Get an ordered list of the repository changeset revisions that are installable", diff --git a/lib/tool_shed/webapp/api2/users.py b/lib/tool_shed/webapp/api2/users.py index 3e57735b6718..8873d8b9800f 100644 --- a/lib/tool_shed/webapp/api2/users.py +++ b/lib/tool_shed/webapp/api2/users.py @@ -1,3 +1,4 @@ +import logging from typing import ( List, Optional, @@ -9,6 +10,10 @@ status, ) from pydantic import BaseModel +from sqlalchemy import ( + and_, + true, +) import tool_shed.util.shed_util_common as suc from galaxy.exceptions import ( @@ -17,12 +22,16 @@ RequestParameterInvalidException, ) from galaxy.managers.api_keys import ApiKeyManager +from galaxy.managers.users import UserManager +from galaxy.webapps.base.webapp import create_new_session from tool_shed.context import SessionRequestContext from tool_shed.managers.users import ( api_create_user, get_api_user, index, ) +from tool_shed.structured_app import ToolShedApp +from tool_shed.webapp.model import User as SaUser from tool_shed_client.schema import ( CreateUserRequest, User, @@ -30,15 +39,64 @@ from . import ( depends, DependsOnTrans, + ensure_valid_session, Router, + set_auth_cookie, UserIdPathParam, ) router = Router(tags=["users"]) +log = logging.getLogger(__name__) + + +class UiRegisterRequest(BaseModel): + email: str + username: str + password: str + bear_field: str + + +class HasCsrfToken(BaseModel): + session_csrf_token: str + + +class UiLoginRequest(HasCsrfToken): + login: str + password: str + + +class UiLogoutRequest(HasCsrfToken): + logout_all: bool = False + + +class UiLoginResponse(BaseModel): + pass + + +class UiLogoutResponse(BaseModel): + pass + + +class UiRegisterResponse(BaseModel): + email: str + activation_sent: bool = False + activation_error: bool = False + contact_email: Optional[str] = None + + +class UiChangePasswordRequest(BaseModel): + current: str + password: str + + +INVALID_LOGIN_OR_PASSWORD = "Invalid login or password" + @router.cbv class FastAPIUsers: + app: ToolShedApp = depends(ToolShedApp) + user_manager: UserManager = depends(UserManager) api_key_manager: ApiKeyManager = depends(ApiKeyManager) @router.get( @@ -66,7 +124,9 @@ def create(self, trans: SessionRequestContext = DependsOnTrans, request: CreateU ) def current(self, trans: SessionRequestContext = DependsOnTrans) -> User: user = trans.user - assert user + if not user: + raise ObjectNotFound() + return get_api_user(trans.app, user) @router.get( @@ -128,3 +188,153 @@ def _get_user(self, trans: SessionRequestContext, encoded_user_id: str): if not (trans.user_is_admin or trans.user == user): raise InsufficientPermissionsException() return user + + @router.post( + "/api_internal/register", + description="register a user", + operation_id="users__internal_register", + ) + def register( + self, trans: SessionRequestContext = DependsOnTrans, request: UiRegisterRequest = Body(...) + ) -> UiRegisterResponse: + honeypot_field = request.bear_field + if honeypot_field != "": + message = "You've been flagged as a possible bot. If you are not, please try registering again and fill the form out carefully." + raise RequestParameterInvalidException(message) + + username = request.username + if username == "repos": + raise RequestParameterInvalidException("Cannot create a user with the username 'repos'") + self.user_manager.create(email=request.email, username=username, password=request.password) + if self.app.config.user_activation_on: + is_activation_sent = self.user_manager.send_activation_email(trans, request.email, username) + if is_activation_sent: + return UiRegisterResponse(email=request.email, activation_sent=True) + else: + return UiRegisterResponse( + email=request.email, + activation_sent=False, + activation_error=True, + contact_email=self.app.config.error_email_to, + ) + else: + return UiRegisterResponse(email=request.email) + + @router.put( + "/api_internal/change_password", + description="reset a user", + operation_id="users__internal_change_password", + status_code=status.HTTP_204_NO_CONTENT, + ) + def change_password( + self, trans: SessionRequestContext = DependsOnTrans, request: UiChangePasswordRequest = Body(...) + ): + password = request.password + current = request.current + if trans.user is None: + raise InsufficientPermissionsException("Must be logged into use this functionality") + user_id = trans.user.id + token = None + user, message = self.user_manager.change_password( + trans, password=password, current=current, token=token, confirm=password, id=user_id + ) + if not user: + raise RequestParameterInvalidException(message) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + @router.put( + "/api_internal/login", + description="login to web UI", + operation_id="users__internal_login", + ) + def internal_login( + self, trans: SessionRequestContext = DependsOnTrans, request: UiLoginRequest = Body(...) + ) -> UiLoginResponse: + log.info(f"top of internal_login {trans.session_csrf_token}") + ensure_csrf_token(trans, request) + login = request.login + password = request.password + user = self.user_manager.get_user_by_identity(login) + if user is None: + raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) + elif user.deleted: + message = ( + "This account has been marked deleted, contact your local Galaxy administrator to restore the account." + ) + if trans.app.config.error_email_to is not None: + message += f" Contact: {trans.app.config.error_email_to}." + raise InsufficientPermissionsException(message) + elif not trans.app.auth_manager.check_password(user, password, trans.request): + raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) + else: + handle_user_login(trans, user) + return UiLoginResponse() + + @router.put( + "/api_internal/logout", + description="logout of web UI", + operation_id="users__internal_logout", + ) + def internal_logout( + self, trans: SessionRequestContext = DependsOnTrans, request: UiLogoutRequest = Body(...) + ) -> UiLogoutResponse: + ensure_csrf_token(trans, request) + handle_user_logout(trans, logout_all=request.logout_all) + return UiLogoutResponse() + + +def ensure_csrf_token(trans: SessionRequestContext, request: HasCsrfToken): + session_csrf_token = request.session_csrf_token + if not trans.session_csrf_token: + ensure_valid_session(trans) + message = None + if not session_csrf_token: + message = "No session token set, denying request." + elif session_csrf_token != trans.session_csrf_token: + log.info(f"{session_csrf_token} != {trans.session_csrf_token}") + message = "Wrong session token found, denying request." + if message: + raise InsufficientPermissionsException(message) + + +def handle_user_login(trans: SessionRequestContext, user: SaUser) -> None: + trans.app.security_agent.create_user_role(user, trans.app) + # Set the previous session + prev_galaxy_session = trans.get_galaxy_session() + if prev_galaxy_session: + prev_galaxy_session.is_valid = False + # Define a new current_session + new_session = create_new_session(trans, prev_galaxy_session, user) + trans.set_galaxy_session(new_session) + trans.sa_session.add_all((prev_galaxy_session, new_session)) + trans.sa_session.flush() + set_auth_cookie(trans, new_session) + + +def handle_user_logout(trans, logout_all=False): + """ + Logout the current user: + - invalidate the current session + - create a new session with no user associated + """ + prev_galaxy_session = trans.get_galaxy_session() + if prev_galaxy_session: + prev_galaxy_session.is_valid = False + new_session = create_new_session(trans, prev_galaxy_session, None) + trans.set_galaxy_session(new_session) + trans.sa_session.add_all((prev_galaxy_session, new_session)) + trans.sa_session.flush() + + galaxy_user_id = prev_galaxy_session.user_id + if logout_all and galaxy_user_id is not None: + for other_galaxy_session in trans.sa_session.query(trans.app.model.GalaxySession).filter( + and_( + trans.app.model.GalaxySession.table.c.user_id == galaxy_user_id, + trans.app.model.GalaxySession.table.c.is_valid == true(), + trans.app.model.GalaxySession.table.c.id != prev_galaxy_session.id, + ) + ): + other_galaxy_session.is_valid = False + trans.sa_session.add(other_galaxy_session) + trans.sa_session.flush() + set_auth_cookie(trans, new_session) diff --git a/lib/tool_shed/webapp/fast_app.py b/lib/tool_shed/webapp/fast_app.py index e1fab8be007f..18af5dcb7708 100644 --- a/lib/tool_shed/webapp/fast_app.py +++ b/lib/tool_shed/webapp/fast_app.py @@ -1,10 +1,27 @@ +import logging +import os +from pathlib import Path from typing import ( Any, + cast, Dict, + Optional, ) from a2wsgi import WSGIMiddleware -from fastapi import FastAPI +from fastapi import ( + Depends, + FastAPI, +) +from fastapi.responses import ( + HTMLResponse, + RedirectResponse, +) +from fastapi.staticfiles import StaticFiles +from starlette_graphene3 import ( + GraphQLApp, + make_graphiql_handler, +) from galaxy.webapps.base.api import ( add_exception_handler, @@ -12,6 +29,14 @@ include_all_package_routers, ) from galaxy.webapps.openapi.utils import get_openapi +from tool_shed.structured_app import ToolShedApp +from tool_shed.webapp.api2 import ( + ensure_valid_session, + get_trans, +) +from tool_shed.webapp.graphql.schema import schema + +log = logging.getLogger(__name__) api_tags_metadata = [ { @@ -33,6 +58,90 @@ {"name": "undocumented", "description": "API routes that have not yet been ported to FastAPI."}, ] +# Set this if asset handling should be sent to vite. +# Run vite with: +# yarn dev +# Start tool shed with: +# TOOL_SHED_VITE_PORT=4040 TOOL_SHED_API_VERSION=v2 ./run_tool_shed.sh +TOOL_SHED_VITE_PORT: Optional[str] = os.environ.get("TOOL_SHED_VITE_PORT", None) +TOOL_SHED_USE_HMR: bool = TOOL_SHED_VITE_PORT is not None +FRONTEND = Path(__file__).parent.resolve() / "frontend" +FRONTEND_DIST = FRONTEND / "dist" + + +def frontend_controller(app): + shed_entry_point = "main.ts" + vite_runtime = "@vite/client" + + def index(trans=Depends(get_trans)): + if TOOL_SHED_USE_HMR: + index = FRONTEND / "index.html" + index_html = index.read_text() + index_html = index_html.replace( + f"""""", + f"""""", + ) + else: + index = FRONTEND_DIST / "index.html" + index_html = index.read_text() + ensure_valid_session(trans) + cookie = trans.session_csrf_token + r: HTMLResponse = cast(HTMLResponse, trans.response) + r.set_cookie("session_csrf_token", cookie) + return index_html + + return app, index + + +def redirect_route(app, from_url: str, to_url: str): + @app.get(from_url) + def redirect(): + return RedirectResponse(to_url) + + +def frontend_route(controller, path): + app, index = controller + app.get(path, response_class=HTMLResponse)(index) + + +def mount_graphql(app: FastAPI, tool_shed_app: ToolShedApp): + context = { + "session": tool_shed_app.model.context, + "security": tool_shed_app.security, + } + g_app = GraphQLApp(schema, on_get=make_graphiql_handler(), context_value=context, root_value=context) + app.mount("/graphql", g_app) + app.mount("/api/graphql", g_app) + + +FRONT_END_ROUTES = [ + "/", + "/admin", + "/login", + "/register", + "/logout_success", + "/login_success", + "/registration_success", + "/help", + "/repositories_by_search", + "/repositories_by_category", + "/repositories_by_category/{category_id}", + "/repositories_by_owner", + "/repositories_by_owner/{username}", + "/repositories/{repository_id}", + "/repositories_search", + "/_component_showcase", + "/user/api_key", + "/user/change_password", + "/view/{username}", + "/view/{username}/{repository_name}", + "/view/{username}/{repository_name}/{changeset_revision}", +] +LEGACY_ROUTES = { + "/user/create": "/register", # for twilltestcase + "/user/login": "/login", # for twilltestcase +} + def initialize_fast_app(gx_webapp, tool_shed_app): app = get_fastapi_instance() @@ -40,6 +149,27 @@ def initialize_fast_app(gx_webapp, tool_shed_app): add_request_id_middleware(app) from .buildapp import SHED_API_VERSION + def mount_static(directory: Path): + name = directory.name + if directory.exists(): + app.mount(f"/{name}", StaticFiles(directory=directory), name=name) + + if SHED_API_VERSION == "v2": + controller = frontend_controller(app) + for route in FRONT_END_ROUTES: + frontend_route(controller, route) + + for from_route, to_route in LEGACY_ROUTES.items(): + redirect_route(app, from_route, to_route) + + mount_graphql(app, tool_shed_app) + + mount_static(FRONTEND / "static") + if TOOL_SHED_USE_HMR: + mount_static(FRONTEND / "node_modules") + else: + mount_static(FRONTEND_DIST / "assets") + routes_package = "tool_shed.webapp.api" if SHED_API_VERSION == "v1" else "tool_shed.webapp.api2" include_all_package_routers(app, routes_package) wsgi_handler = WSGIMiddleware(gx_webapp) diff --git a/lib/tool_shed/webapp/frontend/.eslintignore b/lib/tool_shed/webapp/frontend/.eslintignore new file mode 100644 index 000000000000..b22c816bfd5e --- /dev/null +++ b/lib/tool_shed/webapp/frontend/.eslintignore @@ -0,0 +1,7 @@ +# don't ever lint node_modules +node_modules +# don't lint build output (make sure it's set to your correct build folder name) +dist + +# Ignore codegen aritfacts +src/gql/*.ts diff --git a/lib/tool_shed/webapp/frontend/.eslintrc.js b/lib/tool_shed/webapp/frontend/.eslintrc.js new file mode 100644 index 000000000000..7343e0cc14e1 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + root: true, + parser: "vue-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + // project: ['./tsconfig.json'], + }, + extends: [ + "plugin:vue/strongly-recommended", + "eslint:recommended", + "@vue/typescript/recommended", + "prettier", + "plugin:vuejs-accessibility/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + // More goodies.. + // "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + plugins: ["@typescript-eslint", "prettier", "vuejs-accessibility"], + rules: { + "prettier/prettier": "error", + // not needed for vue 3 + "vue/no-multiple-template-root": "off", + // upgrade warnings for common John problems + "@typescript-eslint/no-unused-vars": "error", + "vue/require-default-prop": "error", + "vue/v-slot-style": "error", + }, +} diff --git a/lib/tool_shed/webapp/frontend/.prettierrc b/lib/tool_shed/webapp/frontend/.prettierrc new file mode 100644 index 000000000000..0fe7f46213c9 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "printWidth": 120, + "semi": false +} diff --git a/lib/tool_shed/webapp/frontend/Makefile b/lib/tool_shed/webapp/frontend/Makefile new file mode 100644 index 000000000000..b520b0be6844 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/Makefile @@ -0,0 +1,22 @@ +GALAXY_ROOT=../../../.. + +client: + yarn build + +dev: + yarn dev-all + +format: + yarn format + +lint: + yarn typecheck && yarn lint + +# These next two tasks don't really belong here, but they do demonstrate +# how to get a test server running and populated with some initial data +# for the new tool shed frontend. +run_test_backend: + cd $(GALAXY_ROOT); TOOL_SHED_CONFIG_OVERRIDE_BOOTSTRAP_ADMIN_API_KEY=tsadminkey TOOL_SHED_VITE_PORT=4040 TOOL_SHED_API_VERSION=v2 ./run_tool_shed.sh + +bootstrap_test_backend: + cd $(GALAXY_ROOT); . .venv/bin/activate; python scripts/bootstrap_test_shed.py diff --git a/lib/tool_shed/webapp/frontend/README.md b/lib/tool_shed/webapp/frontend/README.md new file mode 100644 index 000000000000..a797a275d079 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/README.md @@ -0,0 +1,27 @@ +# Vue 3 + Typescript + Vite + +This template should help get you started developing with Vue 3 and Typescript in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings! + +### If Using ` + + diff --git a/lib/tool_shed/webapp/frontend/package.json b/lib/tool_shed/webapp/frontend/package.json new file mode 100644 index 000000000000..4cf7418d4762 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/package.json @@ -0,0 +1,54 @@ +{ + "name": "galaxy-tool-shed", + "license": "MIT", + "version": "0.2.0", + "scripts": { + "dev": "vite --port 4040 --strict-port", + "build": "vue-tsc --noEmit && vite build", + "graphql": "graphql-codegen --watch", + "dev-all": "concurrently --kill-others \"npm run dev\" \"npm run graphql\"", + "format": "prettier --write src", + "typecheck": "vue-tsc --noEmit", + "lint": "eslint src --ext .ts,.vue" + }, + "devDependencies": { + "@graphql-codegen/cli": "^2.16.1", + "@graphql-codegen/client-preset": "^1.2.3", + "@quasar/vite-plugin": "^1.0.4", + "@types/node": "^16.6.1", + "@typescript-eslint/eslint-plugin": "^5.47.1", + "@typescript-eslint/parser": "^5.47.1", + "@vitejs/plugin-vue": "^1.6.0", + "@vue/compiler-sfc": "^3.2.6", + "@vue/eslint-config-typescript": "^11.0.2", + "concurrently": "^7.6.0", + "eslint": "^8.30.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-vue": "^9.8.0", + "eslint-plugin-vuejs-accessibility": "^2.0.0", + "prettier": "^2.8.1", + "sass": "^1.32.0", + "typescript": "^4.3.2", + "vite": "^2.4.4", + "vue-eslint-parser": "^9.1.0", + "vue-tsc": "^1.0.16" + }, + "dependencies": { + "@apollo/client": "^3.7.3", + "@quasar/extras": "^1.12.4", + "@vue/apollo-composable": "^4.0.0-beta.1", + "@vue/apollo-option": "^4.0.0-alpha.20", + "axios": "^1.2.1", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.7", + "e": "^0.2.2", + "graphql": "^16.6.0", + "graphql-tag": "^2.12.6", + "openapi-typescript-fetch": "^1.1.3", + "pinia": "^2.0.28", + "quasar": "^2.5.0", + "vue": "^3.2.6", + "vue-router": "4" + } +} diff --git a/lib/tool_shed/webapp/frontend/src/App.vue b/lib/tool_shed/webapp/frontend/src/App.vue new file mode 100644 index 000000000000..a11ce54f175e --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/App.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/lib/tool_shed/webapp/frontend/src/apiUtil.ts b/lib/tool_shed/webapp/frontend/src/apiUtil.ts new file mode 100644 index 000000000000..f14774767f97 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/apiUtil.ts @@ -0,0 +1,19 @@ +import axios from "axios" +import { RawAxiosRequestConfig } from "axios" +import { components } from "@/schema" + +type User = components["schemas"]["User"] + +export async function getCurrentUser(): Promise { + const conf: RawAxiosRequestConfig = {} + conf.validateStatus = (status: number) => { + const valid = status == 200 || status == 404 + return valid + } + const { data: user, status } = await axios.get("/api/users/current", conf) + if (status == 404) { + return null + } else { + return user as User + } +} diff --git a/lib/tool_shed/webapp/frontend/src/apollo.ts b/lib/tool_shed/webapp/frontend/src/apollo.ts new file mode 100644 index 000000000000..572617f52fc8 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/apollo.ts @@ -0,0 +1,25 @@ +import { createApolloProvider } from "@vue/apollo-option" +import { ApolloClient, InMemoryCache, DefaultOptions } from "@apollo/client/core" + +const defaultOptions: DefaultOptions = { + watchQuery: { + fetchPolicy: "no-cache", + errorPolicy: "ignore", + }, + query: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, +} + +export const apolloClient = new ApolloClient({ + uri: "/api/graphql/", + cache: new InMemoryCache(), + defaultOptions: defaultOptions, +}) + +export const apolloClientProvider = createApolloProvider({ + defaultClient: apolloClient, +}) + +// npx apollo schema:download --endpoint=http://localhost:9009/graphql/ graphql-schema.json diff --git a/lib/tool_shed/webapp/frontend/src/components/ComponentShowcase.vue b/lib/tool_shed/webapp/frontend/src/components/ComponentShowcase.vue new file mode 100644 index 000000000000..4d07d9cbe897 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ComponentShowcase.vue @@ -0,0 +1,15 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ComponentShowcaseExample.vue b/lib/tool_shed/webapp/frontend/src/components/ComponentShowcaseExample.vue new file mode 100644 index 000000000000..d57104208e6c --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ComponentShowcaseExample.vue @@ -0,0 +1,21 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ConfigFileContents.vue b/lib/tool_shed/webapp/frontend/src/components/ConfigFileContents.vue new file mode 100644 index 000000000000..1720ccf24208 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ConfigFileContents.vue @@ -0,0 +1,36 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ErrorBanner.vue b/lib/tool_shed/webapp/frontend/src/components/ErrorBanner.vue new file mode 100644 index 000000000000..7dfa1aff16e1 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ErrorBanner.vue @@ -0,0 +1,38 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/LoadingDiv.vue b/lib/tool_shed/webapp/frontend/src/components/LoadingDiv.vue new file mode 100644 index 000000000000..907c40210c82 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/LoadingDiv.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/LoginForm.vue b/lib/tool_shed/webapp/frontend/src/components/LoginForm.vue new file mode 100644 index 000000000000..bc1342c5418e --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/LoginForm.vue @@ -0,0 +1,38 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/LoginPage.vue b/lib/tool_shed/webapp/frontend/src/components/LoginPage.vue new file mode 100644 index 000000000000..65e7403bde93 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/LoginPage.vue @@ -0,0 +1,17 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ManagePushAccess.vue b/lib/tool_shed/webapp/frontend/src/components/ManagePushAccess.vue new file mode 100644 index 000000000000..538c8aeb32f9 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ManagePushAccess.vue @@ -0,0 +1,42 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ModalForm.vue b/lib/tool_shed/webapp/frontend/src/components/ModalForm.vue new file mode 100644 index 000000000000..77a34dc8d324 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ModalForm.vue @@ -0,0 +1,23 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/PageContainer.vue b/lib/tool_shed/webapp/frontend/src/components/PageContainer.vue new file mode 100644 index 000000000000..ae392f503046 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/PageContainer.vue @@ -0,0 +1,14 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RecentlyCreatedRepositories.vue b/lib/tool_shed/webapp/frontend/src/components/RecentlyCreatedRepositories.vue new file mode 100644 index 000000000000..934f4e385985 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RecentlyCreatedRepositories.vue @@ -0,0 +1,39 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RecentlyUpdatedRepositories.vue b/lib/tool_shed/webapp/frontend/src/components/RecentlyUpdatedRepositories.vue new file mode 100644 index 000000000000..44454558a79b --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RecentlyUpdatedRepositories.vue @@ -0,0 +1,39 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RegisterPage.vue b/lib/tool_shed/webapp/frontend/src/components/RegisterPage.vue new file mode 100644 index 000000000000..a66cf394f529 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RegisterPage.vue @@ -0,0 +1,82 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RegistrationSuccess.vue b/lib/tool_shed/webapp/frontend/src/components/RegistrationSuccess.vue new file mode 100644 index 000000000000..2d6eaac69728 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RegistrationSuccess.vue @@ -0,0 +1,22 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoriesForOwner.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoriesForOwner.vue new file mode 100644 index 000000000000..28fe8e1f8b35 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoriesForOwner.vue @@ -0,0 +1,67 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoriesGrid.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoriesGrid.vue new file mode 100644 index 000000000000..7cba807d69db --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoriesGrid.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoriesGridInterface.ts b/lib/tool_shed/webapp/frontend/src/components/RepositoriesGridInterface.ts new file mode 100644 index 000000000000..5b9ca505d151 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoriesGridInterface.ts @@ -0,0 +1,36 @@ +import { useFragment } from "@/gql/fragment-masking" +import { RepositoryListItemFragment } from "@/gqlFragements" + +export interface RepositoryGridItem { + id: string + name: string + owner: string + index: number + update_time: string + description: string | null + homepage_url: string | null | undefined + remote_repository_url: string | null | undefined +} + +export type OnScroll = () => Promise + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function nodeToRow(node: any, index: number): RepositoryGridItem { + /* Adapt CQL results to RepositoryGridItem interface consumed by the + component. */ + if (node == null) { + throw Error("Problem with server response") + } + + const fragment = useFragment(RepositoryListItemFragment, node) + return { + id: fragment.encodedId, + index: index, + name: fragment.name as string, // TODO: fix schema.py so this is nonnull + owner: fragment.user.username, + description: fragment.description || null, + homepage_url: fragment.homepageUrl || null, + remote_repository_url: fragment.remoteRepositoryUrl || null, + update_time: fragment.updateTime, + } +} diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryActions.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryActions.vue new file mode 100644 index 000000000000..ccd55676efbd --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryActions.vue @@ -0,0 +1,45 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryCreation.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryCreation.vue new file mode 100644 index 000000000000..8e41a409c1bd --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryCreation.vue @@ -0,0 +1,41 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryExplore.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryExplore.vue new file mode 100644 index 000000000000..994ca74089af --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryExplore.vue @@ -0,0 +1,71 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryHealth.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryHealth.vue new file mode 100644 index 000000000000..0d3f0924f9b5 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryHealth.vue @@ -0,0 +1,28 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryLink.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryLink.vue new file mode 100644 index 000000000000..706ee2143cc2 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryLink.vue @@ -0,0 +1,29 @@ + + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryLinks.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryLinks.vue new file mode 100644 index 000000000000..6a69ebe9e4fa --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryLinks.vue @@ -0,0 +1,41 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryTool.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryTool.vue new file mode 100644 index 000000000000..996c4400f28d --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryTool.vue @@ -0,0 +1,25 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RepositoryUpdate.vue b/lib/tool_shed/webapp/frontend/src/components/RepositoryUpdate.vue new file mode 100644 index 000000000000..e479176d30d6 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RepositoryUpdate.vue @@ -0,0 +1,25 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RevisionActions.vue b/lib/tool_shed/webapp/frontend/src/components/RevisionActions.vue new file mode 100644 index 000000000000..525934331e1e --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RevisionActions.vue @@ -0,0 +1,62 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/RevisionSelect.vue b/lib/tool_shed/webapp/frontend/src/components/RevisionSelect.vue new file mode 100644 index 000000000000..eb559931236d --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/RevisionSelect.vue @@ -0,0 +1,56 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/SelectUser.vue b/lib/tool_shed/webapp/frontend/src/components/SelectUser.vue new file mode 100644 index 000000000000..351418bc895c --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/SelectUser.vue @@ -0,0 +1,62 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/ShedToolbar.vue b/lib/tool_shed/webapp/frontend/src/components/ShedToolbar.vue new file mode 100644 index 000000000000..631d2349f8ac --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/ShedToolbar.vue @@ -0,0 +1,117 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/UtcDate.vue b/lib/tool_shed/webapp/frontend/src/components/UtcDate.vue new file mode 100644 index 000000000000..03a3a0443bed --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/UtcDate.vue @@ -0,0 +1,32 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/AdminControls.vue b/lib/tool_shed/webapp/frontend/src/components/pages/AdminControls.vue new file mode 100644 index 000000000000..14744ddb64d8 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/AdminControls.vue @@ -0,0 +1,23 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/ChangePassword.vue b/lib/tool_shed/webapp/frontend/src/components/pages/ChangePassword.vue new file mode 100644 index 000000000000..de69706dec34 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/ChangePassword.vue @@ -0,0 +1,50 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/CitableRepositoryPage.vue b/lib/tool_shed/webapp/frontend/src/components/pages/CitableRepositoryPage.vue new file mode 100644 index 000000000000..bbf717fecde4 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/CitableRepositoryPage.vue @@ -0,0 +1,44 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/ComponentsShowcase.vue b/lib/tool_shed/webapp/frontend/src/components/pages/ComponentsShowcase.vue new file mode 100644 index 000000000000..65786575671a --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/ComponentsShowcase.vue @@ -0,0 +1,60 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/HelpPage.vue b/lib/tool_shed/webapp/frontend/src/components/pages/HelpPage.vue new file mode 100644 index 000000000000..adb927f6331c --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/HelpPage.vue @@ -0,0 +1,20 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/LandingPage.vue b/lib/tool_shed/webapp/frontend/src/components/pages/LandingPage.vue new file mode 100644 index 000000000000..5e04f26eeb58 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/LandingPage.vue @@ -0,0 +1,25 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/ManageApiKey.vue b/lib/tool_shed/webapp/frontend/src/components/pages/ManageApiKey.vue new file mode 100644 index 000000000000..74d7c55980f9 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/ManageApiKey.vue @@ -0,0 +1,84 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategories.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategories.vue new file mode 100644 index 000000000000..54e7addc4e53 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategories.vue @@ -0,0 +1,44 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategory.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategory.vue new file mode 100644 index 000000000000..b57b82ecc98a --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByCategory.vue @@ -0,0 +1,85 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwner.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwner.vue new file mode 100644 index 000000000000..765e7e64d7ec --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwner.vue @@ -0,0 +1,15 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwners.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwners.vue new file mode 100644 index 000000000000..8b91b25fce02 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesByOwners.vue @@ -0,0 +1,36 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesBySearch.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesBySearch.vue new file mode 100644 index 000000000000..a2a87875cc5f --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoriesBySearch.vue @@ -0,0 +1,88 @@ + + diff --git a/lib/tool_shed/webapp/frontend/src/components/pages/RepositoryPage.vue b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoryPage.vue new file mode 100644 index 000000000000..6e31d63fc2d0 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/components/pages/RepositoryPage.vue @@ -0,0 +1,277 @@ + + + diff --git a/lib/tool_shed/webapp/frontend/src/constants.ts b/lib/tool_shed/webapp/frontend/src/constants.ts new file mode 100644 index 000000000000..b3a88733c8c6 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/constants.ts @@ -0,0 +1,13 @@ +export const UPDATING_WITH_PLANEMO_URL = + "https://planemo.readthedocs.io/en/latest/publishing.html#updating-a-repository" + +export const EPHEMERIS_TRAINING = + "https://training.galaxyproject.org/training-material/topics/admin/tutorials/tool-management/tutorial.html" + +export const AUTH_FORM_INPUT_PROPS = { + square: true, + clearable: false, + // choose filled or outlined or neither I think? + outlined: true, + filled: false, +} diff --git a/lib/tool_shed/webapp/frontend/src/gql/fragment-masking.ts b/lib/tool_shed/webapp/frontend/src/gql/fragment-masking.ts new file mode 100644 index 000000000000..f11e66ee57fc --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/gql/fragment-masking.ts @@ -0,0 +1,50 @@ +import { TypedDocumentNode as DocumentNode, ResultOf } from "@graphql-typed-document-node/core" + +export type FragmentType> = TDocumentType extends DocumentNode< + infer TType, + any +> + ? TType extends { " $fragmentName"?: infer TKey } + ? TKey extends string + ? { " $fragmentRefs"?: { [key in TKey]: TType } } + : never + : never + : never + +// return non-nullable if `fragmentType` is non-nullable +export function useFragment( + _documentNode: DocumentNode, + fragmentType: FragmentType> +): TType +// return nullable if `fragmentType` is nullable +export function useFragment( + _documentNode: DocumentNode, + fragmentType: FragmentType> | null | undefined +): TType | null | undefined +// return array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentNode, + fragmentType: ReadonlyArray>> +): ReadonlyArray +// return array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentNode, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined +export function useFragment( + _documentNode: DocumentNode, + fragmentType: + | FragmentType> + | ReadonlyArray>> + | null + | undefined +): TType | ReadonlyArray | null | undefined { + return fragmentType as any +} + +export function makeFragmentData>( + data: FT, + _fragment: F +): FragmentType { + return data as FragmentType +} diff --git a/lib/tool_shed/webapp/frontend/src/gql/gql.ts b/lib/tool_shed/webapp/frontend/src/gql/gql.ts new file mode 100644 index 000000000000..0793f6959859 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/gql/gql.ts @@ -0,0 +1,98 @@ +/* eslint-disable */ +import * as types from "./graphql" +import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core" + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel-plugin for production. + */ +const documents = { + "\n query recentlyCreatedRepositories {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryCreationItem\n }\n }\n }\n }\n": + types.RecentlyCreatedRepositoriesDocument, + "\n query recentRepositoryUpdates {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryUpdateItem\n }\n }\n }\n }\n": + types.RecentRepositoryUpdatesDocument, + "\n query repositoriesByOwner($username: String, $cursor: String) {\n relayRepositoriesForOwner(username: $username, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": + types.RepositoriesByOwnerDocument, + "\n fragment RepositoryCreationItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n createTime\n }\n": + types.RepositoryCreationItemFragmentDoc, + "\n query repositoriesByCategory($categoryId: String, $cursor: String) {\n relayRepositoriesForCategory(encodedId: $categoryId, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": + types.RepositoriesByCategoryDocument, + "\n fragment RepositoryListItemFragment on RelayRepository {\n encodedId\n name\n user {\n username\n }\n description\n type\n updateTime\n homepageUrl\n remoteRepositoryUrl\n }\n": + types.RepositoryListItemFragmentFragmentDoc, + "\n fragment RepositoryUpdateItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n updateTime\n }\n": + types.RepositoryUpdateItemFragmentDoc, +} + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function graphql(source: string): unknown + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query recentlyCreatedRepositories {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryCreationItem\n }\n }\n }\n }\n" +): (typeof documents)["\n query recentlyCreatedRepositories {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryCreationItem\n }\n }\n }\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query recentRepositoryUpdates {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryUpdateItem\n }\n }\n }\n }\n" +): (typeof documents)["\n query recentRepositoryUpdates {\n relayRepositories(first: 10, sort: UPDATE_TIME_DESC) {\n edges {\n node {\n ...RepositoryUpdateItem\n }\n }\n }\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query repositoriesByOwner($username: String, $cursor: String) {\n relayRepositoriesForOwner(username: $username, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n" +): (typeof documents)["\n query repositoriesByOwner($username: String, $cursor: String) {\n relayRepositoriesForOwner(username: $username, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n fragment RepositoryCreationItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n createTime\n }\n" +): (typeof documents)["\n fragment RepositoryCreationItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n createTime\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query repositoriesByCategory($categoryId: String, $cursor: String) {\n relayRepositoriesForCategory(encodedId: $categoryId, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n" +): (typeof documents)["\n query repositoriesByCategory($categoryId: String, $cursor: String) {\n relayRepositoriesForCategory(encodedId: $categoryId, sort: UPDATE_TIME_DESC, first: 10, after: $cursor) {\n edges {\n cursor\n node {\n ...RepositoryListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n fragment RepositoryListItemFragment on RelayRepository {\n encodedId\n name\n user {\n username\n }\n description\n type\n updateTime\n homepageUrl\n remoteRepositoryUrl\n }\n" +): (typeof documents)["\n fragment RepositoryListItemFragment on RelayRepository {\n encodedId\n name\n user {\n username\n }\n description\n type\n updateTime\n homepageUrl\n remoteRepositoryUrl\n }\n"] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n fragment RepositoryUpdateItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n updateTime\n }\n" +): (typeof documents)["\n fragment RepositoryUpdateItem on RelayRepository {\n encodedId\n name\n user {\n username\n }\n updateTime\n }\n"] + +export function graphql(source: string) { + return (documents as any)[source] ?? {} +} + +export type DocumentType> = TDocumentNode extends DocumentNode< + infer TType, + any +> + ? TType + : never diff --git a/lib/tool_shed/webapp/frontend/src/gql/graphql.ts b/lib/tool_shed/webapp/frontend/src/gql/graphql.ts new file mode 100644 index 000000000000..f6c3433eb96b --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/gql/graphql.ts @@ -0,0 +1,821 @@ +/* eslint-disable */ +import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core" +export type Maybe = T | null +export type InputMaybe = Maybe +export type Exact = { [K in keyof T]: T[K] } +export type MakeOptional = Omit & { [SubKey in K]?: Maybe } +export type MakeMaybe = Omit & { [SubKey in K]: Maybe } +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string + String: string + Boolean: boolean + Int: number + Float: number + /** + * The `DateTime` scalar type represents a DateTime + * value as specified by + * [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + */ + DateTime: any +} + +/** An object with an ID */ +export type Node = { + /** The ID of the object */ + id: Scalars["ID"] +} + +/** The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. */ +export type PageInfo = { + __typename?: "PageInfo" + /** When paginating forwards, the cursor to continue. */ + endCursor?: Maybe + /** When paginating forwards, are there more items? */ + hasNextPage: Scalars["Boolean"] + /** When paginating backwards, are there more items? */ + hasPreviousPage: Scalars["Boolean"] + /** When paginating backwards, the cursor to continue. */ + startCursor?: Maybe +} + +export type Query = { + __typename?: "Query" + categories?: Maybe>> + node?: Maybe + relayCategories?: Maybe + relayRepositories?: Maybe + relayRepositoriesForCategory?: Maybe + relayRepositoriesForOwner?: Maybe + relayRevisions?: Maybe + relayUsers?: Maybe + repositories?: Maybe>> + revisions?: Maybe>> + users?: Maybe>> +} + +export type QueryNodeArgs = { + id: Scalars["ID"] +} + +export type QueryRelayCategoriesArgs = { + after?: InputMaybe + before?: InputMaybe + first?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> +} + +export type QueryRelayRepositoriesArgs = { + after?: InputMaybe + before?: InputMaybe + first?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> +} + +export type QueryRelayRepositoriesForCategoryArgs = { + after?: InputMaybe + before?: InputMaybe + encodedId?: InputMaybe + first?: InputMaybe + id?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> +} + +export type QueryRelayRepositoriesForOwnerArgs = { + after?: InputMaybe + before?: InputMaybe + first?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> + username?: InputMaybe +} + +export type QueryRelayRevisionsArgs = { + after?: InputMaybe + before?: InputMaybe + first?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> +} + +export type QueryRelayUsersArgs = { + after?: InputMaybe + before?: InputMaybe + first?: InputMaybe + last?: InputMaybe + sort?: InputMaybe>> +} + +export type RelayCategory = Node & { + __typename?: "RelayCategory" + createTime?: Maybe + deleted?: Maybe + description?: Maybe + encodedId: Scalars["String"] + id: Scalars["ID"] + name: Scalars["String"] + repositories?: Maybe>> + updateTime?: Maybe +} + +export type RelayCategoryConnection = { + __typename?: "RelayCategoryConnection" + /** Contains the nodes in this connection. */ + edges: Array> + /** Pagination data for this connection. */ + pageInfo: PageInfo +} + +/** A Relay edge containing a `RelayCategory` and its cursor. */ +export type RelayCategoryEdge = { + __typename?: "RelayCategoryEdge" + /** A cursor for use in pagination */ + cursor: Scalars["String"] + /** The item at the end of the edge */ + node?: Maybe +} + +/** An enumeration. */ +export enum RelayCategorySortEnum { + CreateTimeAsc = "CREATE_TIME_ASC", + CreateTimeDesc = "CREATE_TIME_DESC", + DeletedAsc = "DELETED_ASC", + DeletedDesc = "DELETED_DESC", + DescriptionAsc = "DESCRIPTION_ASC", + DescriptionDesc = "DESCRIPTION_DESC", + IdAsc = "ID_ASC", + IdDesc = "ID_DESC", + NameAsc = "NAME_ASC", + NameDesc = "NAME_DESC", + UpdateTimeAsc = "UPDATE_TIME_ASC", + UpdateTimeDesc = "UPDATE_TIME_DESC", +} + +export type RelayRepository = Node & { + __typename?: "RelayRepository" + categories?: Maybe>> + createTime?: Maybe + description?: Maybe + encodedId: Scalars["String"] + homepageUrl?: Maybe + id: Scalars["ID"] + longDescription?: Maybe + name: Scalars["String"] + remoteRepositoryUrl?: Maybe + type?: Maybe + updateTime?: Maybe + user: SimpleUser +} + +export type RelayRepositoryConnection = { + __typename?: "RelayRepositoryConnection" + /** Contains the nodes in this connection. */ + edges: Array> + /** Pagination data for this connection. */ + pageInfo: PageInfo +} + +/** A Relay edge containing a `RelayRepository` and its cursor. */ +export type RelayRepositoryEdge = { + __typename?: "RelayRepositoryEdge" + /** A cursor for use in pagination */ + cursor: Scalars["String"] + /** The item at the end of the edge */ + node?: Maybe +} + +export type RelayRepositoryMetadata = Node & { + __typename?: "RelayRepositoryMetadata" + changesetRevision: Scalars["String"] + createTime?: Maybe + downloadable?: Maybe + encodedId: Scalars["String"] + id: Scalars["ID"] + malicious?: Maybe + numericRevision?: Maybe + repository: SimpleRepository + updateTime?: Maybe +} + +export type RelayRepositoryMetadataConnection = { + __typename?: "RelayRepositoryMetadataConnection" + /** Contains the nodes in this connection. */ + edges: Array> + /** Pagination data for this connection. */ + pageInfo: PageInfo +} + +/** A Relay edge containing a `RelayRepositoryMetadata` and its cursor. */ +export type RelayRepositoryMetadataEdge = { + __typename?: "RelayRepositoryMetadataEdge" + /** A cursor for use in pagination */ + cursor: Scalars["String"] + /** The item at the end of the edge */ + node?: Maybe +} + +/** An enumeration. */ +export enum RelayRepositoryMetadataSortEnum { + IdAsc = "ID_ASC", + IdDesc = "ID_DESC", +} + +/** An enumeration. */ +export enum RelayRepositorySortEnum { + CreateTimeAsc = "CREATE_TIME_ASC", + CreateTimeDesc = "CREATE_TIME_DESC", + DescriptionAsc = "DESCRIPTION_ASC", + DescriptionDesc = "DESCRIPTION_DESC", + HomepageUrlAsc = "HOMEPAGE_URL_ASC", + HomepageUrlDesc = "HOMEPAGE_URL_DESC", + IdAsc = "ID_ASC", + IdDesc = "ID_DESC", + LongDescriptionAsc = "LONG_DESCRIPTION_ASC", + LongDescriptionDesc = "LONG_DESCRIPTION_DESC", + NameAsc = "NAME_ASC", + NameDesc = "NAME_DESC", + RemoteRepositoryUrlAsc = "REMOTE_REPOSITORY_URL_ASC", + RemoteRepositoryUrlDesc = "REMOTE_REPOSITORY_URL_DESC", + TypeAsc = "TYPE_ASC", + TypeDesc = "TYPE_DESC", + UpdateTimeAsc = "UPDATE_TIME_ASC", + UpdateTimeDesc = "UPDATE_TIME_DESC", +} + +export type RelayUser = Node & { + __typename?: "RelayUser" + encodedId: Scalars["String"] + id: Scalars["ID"] + username: Scalars["String"] +} + +export type RelayUserConnection = { + __typename?: "RelayUserConnection" + /** Contains the nodes in this connection. */ + edges: Array> + /** Pagination data for this connection. */ + pageInfo: PageInfo +} + +/** A Relay edge containing a `RelayUser` and its cursor. */ +export type RelayUserEdge = { + __typename?: "RelayUserEdge" + /** A cursor for use in pagination */ + cursor: Scalars["String"] + /** The item at the end of the edge */ + node?: Maybe +} + +/** An enumeration. */ +export enum RelayUserSortEnum { + IdAsc = "ID_ASC", + IdDesc = "ID_DESC", + UsernameAsc = "USERNAME_ASC", + UsernameDesc = "USERNAME_DESC", +} + +export type SimpleCategory = { + __typename?: "SimpleCategory" + createTime?: Maybe + deleted?: Maybe + description?: Maybe + encodedId: Scalars["String"] + id: Scalars["ID"] + name: Scalars["String"] + repositories?: Maybe>> + updateTime?: Maybe +} + +export type SimpleRepository = { + __typename?: "SimpleRepository" + categories?: Maybe>> + createTime?: Maybe + description?: Maybe + downloadableRevisions?: Maybe>> + encodedId: Scalars["String"] + homepageUrl?: Maybe + id: Scalars["ID"] + longDescription?: Maybe + metadataRevisions?: Maybe>> + name: Scalars["String"] + remoteRepositoryUrl?: Maybe + type?: Maybe + updateTime?: Maybe + user: SimpleUser +} + +export type SimpleRepositoryMetadata = { + __typename?: "SimpleRepositoryMetadata" + changesetRevision: Scalars["String"] + createTime?: Maybe + downloadable?: Maybe + encodedId: Scalars["String"] + id: Scalars["ID"] + malicious?: Maybe + numericRevision?: Maybe + repository: SimpleRepository + updateTime?: Maybe +} + +export type SimpleUser = { + __typename?: "SimpleUser" + encodedId: Scalars["String"] + id: Scalars["ID"] + username: Scalars["String"] +} + +export type RecentlyCreatedRepositoriesQueryVariables = Exact<{ [key: string]: never }> + +export type RecentlyCreatedRepositoriesQuery = { + __typename?: "Query" + relayRepositories?: { + __typename?: "RelayRepositoryConnection" + edges: Array<{ + __typename?: "RelayRepositoryEdge" + node?: + | ({ __typename?: "RelayRepository" } & { + " $fragmentRefs"?: { RepositoryCreationItemFragment: RepositoryCreationItemFragment } + }) + | null + } | null> + } | null +} + +export type RecentRepositoryUpdatesQueryVariables = Exact<{ [key: string]: never }> + +export type RecentRepositoryUpdatesQuery = { + __typename?: "Query" + relayRepositories?: { + __typename?: "RelayRepositoryConnection" + edges: Array<{ + __typename?: "RelayRepositoryEdge" + node?: + | ({ __typename?: "RelayRepository" } & { + " $fragmentRefs"?: { RepositoryUpdateItemFragment: RepositoryUpdateItemFragment } + }) + | null + } | null> + } | null +} + +export type RepositoriesByOwnerQueryVariables = Exact<{ + username?: InputMaybe + cursor?: InputMaybe +}> + +export type RepositoriesByOwnerQuery = { + __typename?: "Query" + relayRepositoriesForOwner?: { + __typename?: "RelayRepositoryConnection" + edges: Array<{ + __typename?: "RelayRepositoryEdge" + cursor: string + node?: + | ({ __typename?: "RelayRepository" } & { + " $fragmentRefs"?: { RepositoryListItemFragmentFragment: RepositoryListItemFragmentFragment } + }) + | null + } | null> + pageInfo: { __typename?: "PageInfo"; endCursor?: string | null; hasNextPage: boolean } + } | null +} + +export type RepositoryCreationItemFragment = { + __typename?: "RelayRepository" + encodedId: string + name: string + createTime?: any | null + user: { __typename?: "SimpleUser"; username: string } +} & { " $fragmentName"?: "RepositoryCreationItemFragment" } + +export type RepositoriesByCategoryQueryVariables = Exact<{ + categoryId?: InputMaybe + cursor?: InputMaybe +}> + +export type RepositoriesByCategoryQuery = { + __typename?: "Query" + relayRepositoriesForCategory?: { + __typename?: "RelayRepositoryConnection" + edges: Array<{ + __typename?: "RelayRepositoryEdge" + cursor: string + node?: + | ({ __typename?: "RelayRepository" } & { + " $fragmentRefs"?: { RepositoryListItemFragmentFragment: RepositoryListItemFragmentFragment } + }) + | null + } | null> + pageInfo: { __typename?: "PageInfo"; endCursor?: string | null; hasNextPage: boolean } + } | null +} + +export type RepositoryListItemFragmentFragment = { + __typename?: "RelayRepository" + encodedId: string + name: string + description?: string | null + type?: string | null + updateTime?: any | null + homepageUrl?: string | null + remoteRepositoryUrl?: string | null + user: { __typename?: "SimpleUser"; username: string } +} & { " $fragmentName"?: "RepositoryListItemFragmentFragment" } + +export type RepositoryUpdateItemFragment = { + __typename?: "RelayRepository" + encodedId: string + name: string + updateTime?: any | null + user: { __typename?: "SimpleUser"; username: string } +} & { " $fragmentName"?: "RepositoryUpdateItemFragment" } + +export const RepositoryCreationItemFragmentDoc = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "RepositoryCreationItem" }, + typeCondition: { kind: "NamedType", name: { kind: "Name", value: "RelayRepository" } }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "encodedId" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + { + kind: "Field", + name: { kind: "Name", value: "user" }, + selectionSet: { + kind: "SelectionSet", + selections: [{ kind: "Field", name: { kind: "Name", value: "username" } }], + }, + }, + { kind: "Field", name: { kind: "Name", value: "createTime" } }, + ], + }, + }, + ], +} as unknown as DocumentNode +export const RepositoryListItemFragmentFragmentDoc = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "RepositoryListItemFragment" }, + typeCondition: { kind: "NamedType", name: { kind: "Name", value: "RelayRepository" } }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "encodedId" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + { + kind: "Field", + name: { kind: "Name", value: "user" }, + selectionSet: { + kind: "SelectionSet", + selections: [{ kind: "Field", name: { kind: "Name", value: "username" } }], + }, + }, + { kind: "Field", name: { kind: "Name", value: "description" } }, + { kind: "Field", name: { kind: "Name", value: "type" } }, + { kind: "Field", name: { kind: "Name", value: "updateTime" } }, + { kind: "Field", name: { kind: "Name", value: "homepageUrl" } }, + { kind: "Field", name: { kind: "Name", value: "remoteRepositoryUrl" } }, + ], + }, + }, + ], +} as unknown as DocumentNode +export const RepositoryUpdateItemFragmentDoc = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "RepositoryUpdateItem" }, + typeCondition: { kind: "NamedType", name: { kind: "Name", value: "RelayRepository" } }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "encodedId" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + { + kind: "Field", + name: { kind: "Name", value: "user" }, + selectionSet: { + kind: "SelectionSet", + selections: [{ kind: "Field", name: { kind: "Name", value: "username" } }], + }, + }, + { kind: "Field", name: { kind: "Name", value: "updateTime" } }, + ], + }, + }, + ], +} as unknown as DocumentNode +export const RecentlyCreatedRepositoriesDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "recentlyCreatedRepositories" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "relayRepositories" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "10" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "sort" }, + value: { kind: "EnumValue", value: "UPDATE_TIME_DESC" }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "FragmentSpread", + name: { kind: "Name", value: "RepositoryCreationItem" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...RepositoryCreationItemFragmentDoc.definitions, + ], +} as unknown as DocumentNode +export const RecentRepositoryUpdatesDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "recentRepositoryUpdates" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "relayRepositories" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "10" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "sort" }, + value: { kind: "EnumValue", value: "UPDATE_TIME_DESC" }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "FragmentSpread", + name: { kind: "Name", value: "RepositoryUpdateItem" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...RepositoryUpdateItemFragmentDoc.definitions, + ], +} as unknown as DocumentNode +export const RepositoriesByOwnerDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "repositoriesByOwner" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { kind: "Variable", name: { kind: "Name", value: "username" } }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + { + kind: "VariableDefinition", + variable: { kind: "Variable", name: { kind: "Name", value: "cursor" } }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "relayRepositoriesForOwner" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "username" }, + value: { kind: "Variable", name: { kind: "Name", value: "username" } }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "sort" }, + value: { kind: "EnumValue", value: "UPDATE_TIME_DESC" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "10" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "after" }, + value: { kind: "Variable", name: { kind: "Name", value: "cursor" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "cursor" } }, + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "FragmentSpread", + name: { kind: "Name", value: "RepositoryListItemFragment" }, + }, + ], + }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "pageInfo" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "endCursor" } }, + { kind: "Field", name: { kind: "Name", value: "hasNextPage" } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...RepositoryListItemFragmentFragmentDoc.definitions, + ], +} as unknown as DocumentNode +export const RepositoriesByCategoryDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "repositoriesByCategory" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { kind: "Variable", name: { kind: "Name", value: "categoryId" } }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + { + kind: "VariableDefinition", + variable: { kind: "Variable", name: { kind: "Name", value: "cursor" } }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "relayRepositoriesForCategory" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "encodedId" }, + value: { kind: "Variable", name: { kind: "Name", value: "categoryId" } }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "sort" }, + value: { kind: "EnumValue", value: "UPDATE_TIME_DESC" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "10" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "after" }, + value: { kind: "Variable", name: { kind: "Name", value: "cursor" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "cursor" } }, + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "FragmentSpread", + name: { kind: "Name", value: "RepositoryListItemFragment" }, + }, + ], + }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "pageInfo" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "endCursor" } }, + { kind: "Field", name: { kind: "Name", value: "hasNextPage" } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...RepositoryListItemFragmentFragmentDoc.definitions, + ], +} as unknown as DocumentNode diff --git a/lib/tool_shed/webapp/frontend/src/gql/index.ts b/lib/tool_shed/webapp/frontend/src/gql/index.ts new file mode 100644 index 000000000000..a5ba68ae4aee --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/gql/index.ts @@ -0,0 +1,2 @@ +export * from "./gql" +export * from "./fragment-masking" diff --git a/lib/tool_shed/webapp/frontend/src/gqlFragements.ts b/lib/tool_shed/webapp/frontend/src/gqlFragements.ts new file mode 100644 index 000000000000..4eca9379d0da --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/gqlFragements.ts @@ -0,0 +1,27 @@ +import { graphql } from "@/gql" + +export const RepositoryListItemFragment = graphql(/* GraphQL */ ` + fragment RepositoryListItemFragment on RelayRepository { + encodedId + name + user { + username + } + description + type + updateTime + homepageUrl + remoteRepositoryUrl + } +`) + +export const UpdateFragment = graphql(/* GraphQL */ ` + fragment RepositoryUpdateItem on RelayRepository { + encodedId + name + user { + username + } + updateTime + } +`) diff --git a/lib/tool_shed/webapp/frontend/src/main.ts b/lib/tool_shed/webapp/frontend/src/main.ts new file mode 100644 index 000000000000..4130acc25b72 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/main.ts @@ -0,0 +1,24 @@ +import { createApp } from "vue" +import { Quasar, Notify, Cookies } from "quasar" +import App from "./App.vue" +// ( + props: TProps, + emit: (event: string, value: TProps[TKey]) => void, + name: TKey = "modelValue" as TKey +): WritableComputedRef { + return computed({ + get: () => props[name], + set: (value: TProps[TKey]) => { + emit("update:modelValue", value) + }, + }) +} diff --git a/lib/tool_shed/webapp/frontend/src/quasar-variables.sass b/lib/tool_shed/webapp/frontend/src/quasar-variables.sass new file mode 100644 index 000000000000..42043a4e6e4f --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/quasar-variables.sass @@ -0,0 +1,15 @@ +$primary : #2c3143 +$secondary : #dee2e6 + +// really struggling to create constrast in visually +// appealing ways... some failed experiments +// #ffdb58 // #a6c9e1 +$accent : #63a0ca + +$dark : #a3aac4 +$dark-page : #121212 + +$positive : #66cc66 +$negative : #e31a1e +$info : #2077b3 +$warning : #fe7f02 diff --git a/lib/tool_shed/webapp/frontend/src/router.ts b/lib/tool_shed/webapp/frontend/src/router.ts new file mode 100644 index 000000000000..61518786abd9 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/router.ts @@ -0,0 +1,13 @@ +import { createRouter, createWebHistory } from "vue-router" +import routes from "@/routes" + +const router = createRouter({ + history: createWebHistory(), + routes: routes, +}) + +export function goToRepository(id: string) { + router.push(`/repositories/${id}`) +} + +export default router diff --git a/lib/tool_shed/webapp/frontend/src/routes.ts b/lib/tool_shed/webapp/frontend/src/routes.ts new file mode 100644 index 000000000000..5dc76636fd81 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/routes.ts @@ -0,0 +1,113 @@ +import AdminControls from "@/components/pages/AdminControls.vue" +import LandingPage from "@/components/pages/LandingPage.vue" +import LoginPage from "@/components/LoginPage.vue" +import RegisterPage from "@/components/RegisterPage.vue" +import RegistrationSuccess from "@/components/RegistrationSuccess.vue" +import HelpPage from "@/components/pages/HelpPage.vue" +import RepositoriesByCategories from "@/components/pages/RepositoriesByCategories.vue" +import RepositoriesByOwners from "@/components/pages/RepositoriesByOwners.vue" +import RepositoriesByOwner from "@/components/pages/RepositoriesByOwner.vue" +import RepositoriesBySearch from "@/components/pages/RepositoriesBySearch.vue" +import RepositoriesByCategory from "@/components/pages/RepositoriesByCategory.vue" +import ComponentsShowcase from "@/components/pages/ComponentsShowcase.vue" +import RepositoryPage from "@/components/pages/RepositoryPage.vue" +import ManageApiKey from "@/components/pages/ManageApiKey.vue" +import ChangePassword from "@/components/pages/ChangePassword.vue" +import CitableRepositoryPage from "@/components/pages/CitableRepositoryPage.vue" + +import type { RouteRecordRaw } from "vue-router" + +const routes: Array = [ + { + path: "/", + component: LandingPage, + }, + { + path: "/register", + component: RegisterPage, + }, + { + path: "/login", + component: LoginPage, + }, + { + path: "/registration_success", + component: RegistrationSuccess, + }, + { + path: "/login_success", + component: LandingPage, + props: { message: "Login successful!" }, + }, + { + path: "/logout_success", + component: LandingPage, + props: { message: "Logout successful!" }, + }, + { + path: "/help", + component: HelpPage, + }, + { + path: "/admin", + component: AdminControls, + }, + { + path: "/_component_showcase", + component: ComponentsShowcase, + }, + { + path: "/repositories_by_search", + component: RepositoriesBySearch, + }, + { + path: "/repositories_by_category", + component: RepositoriesByCategories, + }, + { + path: "/repositories_by_owner", + component: RepositoriesByOwners, + }, + { + path: "/repositories_by_owner/:username", + component: RepositoriesByOwner, + props: true, + }, + { + path: "/repositories_by_category/:categoryId", + component: RepositoriesByCategory, + props: true, + }, + { + path: "/repositories/:repositoryId", + component: RepositoryPage, + props: true, + }, + { + path: "/user/api_key", + component: ManageApiKey, + }, + { + path: "/user/change_password", + component: ChangePassword, + }, + // legacy style access - was thought of as a citable URL + // so lets keep this path. + { + path: "/view/:username", + component: RepositoriesByOwner, + props: true, + }, + { + path: "/view/:username/:repositoryName", + component: CitableRepositoryPage, + props: true, + }, + { + path: "/view/:username/:repositoryName/:changesetRevision", + component: CitableRepositoryPage, + props: true, + }, +] + +export default routes diff --git a/lib/tool_shed/webapp/frontend/src/schema/fetcher.ts b/lib/tool_shed/webapp/frontend/src/schema/fetcher.ts new file mode 100644 index 000000000000..bb88f1cd5cbd --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/schema/fetcher.ts @@ -0,0 +1,20 @@ +import { Fetcher } from "openapi-typescript-fetch" +import type { paths } from "./schema" + +/* +import type { Middleware } from "openapi-typescript-fetch"; +import { rethrowSimple } from "@/utils/simple-error"; +const rethrowSimpleMiddleware: Middleware = async (url, init, next) => { + try { + const response = await next(url, init); + return response; + } catch (e) { + rethrowSimple(e); + } +}; + +use: [rethrowSimpleMiddleware] +*/ + +export const fetcher = Fetcher.for() +fetcher.configure({ baseUrl: "" }) diff --git a/lib/tool_shed/webapp/frontend/src/schema/index.ts b/lib/tool_shed/webapp/frontend/src/schema/index.ts new file mode 100644 index 000000000000..f334fdb0d2a2 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/schema/index.ts @@ -0,0 +1,3 @@ +export type { components, operations, paths } from "./schema" +export { fetcher } from "./fetcher" +export type { RepositoryTool, RevisionMetadata } from "./types" diff --git a/lib/tool_shed/webapp/frontend/src/schema/schema.ts b/lib/tool_shed/webapp/frontend/src/schema/schema.ts new file mode 100644 index 000000000000..5b2011c4db4f --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/schema/schema.ts @@ -0,0 +1,2061 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/authenticate/baseauth": { + /** Returns returns an API key for authenticated user based on BaseAuth headers. */ + get: operations["authenticate__baseauth"] + } + "/api/categories": { + /** + * Index + * @description index category + */ + get: operations["categories__index"] + /** + * Create + * @description create a category + */ + post: operations["categories__create"] + } + "/api/categories/{encoded_category_id}/repositories": { + /** + * Repositories + * @description display repositories by category + */ + get: operations["categories__repositories"] + } + "/api/ga4gh/trs/v2/service-info": { + /** Service Info */ + get: operations["tools_trs_service_info"] + } + "/api/ga4gh/trs/v2/toolClasses": { + /** Tool Classes */ + get: operations["tools__trs_tool_classes"] + } + "/api/ga4gh/trs/v2/tools": { + /** Trs Index */ + get: operations["tools__trs_index"] + } + "/api/ga4gh/trs/v2/tools/{tool_id}": { + /** Trs Get */ + get: operations["tools__trs_get"] + } + "/api/ga4gh/trs/v2/tools/{tool_id}/versions": { + /** Trs Get Versions */ + get: operations["tools__trs_get_versions"] + } + "/api/repositories": { + /** + * Index + * @description Get a list of repositories or perform a search. + */ + get: operations["repositories__index"] + /** + * Create + * @description create a new repository + */ + post: operations["repositories__create"] + } + "/api/repositories/get_ordered_installable_revisions": { + /** + * Get Ordered Installable Revisions + * @description Get an ordered list of the repository changeset revisions that are installable + */ + get: operations["repositories__get_ordered_installable_revisions"] + } + "/api/repositories/get_repository_revision_install_info": { + /** + * Legacy Install Info + * @description Get information used by the install client to install this repository. + */ + get: operations["repositories__legacy_install_info"] + } + "/api/repositories/install_info": { + /** + * Install Info + * @description Get information used by the install client to install this repository. + */ + get: operations["repositories__install_info"] + } + "/api/repositories/reset_metadata_on_repository": { + /** + * Reset Metadata On Repository Legacy + * @description reset metadata on a repository + */ + post: operations["repositories__reset_legacy"] + } + "/api/repositories/updates": { + /** Updates */ + get: operations["repositories__update"] + } + "/api/repositories/{encoded_repository_id}": { + /** Show */ + get: operations["repositories__show"] + } + "/api/repositories/{encoded_repository_id}/allow_push": { + /** Show Allow Push */ + get: operations["repositories__show_allow_push"] + } + "/api/repositories/{encoded_repository_id}/allow_push/{username}": { + /** Add Allow Push */ + post: operations["repositories__add_allow_push"] + /** Remove Allow Push */ + delete: operations["repositories__remove_allow_push"] + } + "/api/repositories/{encoded_repository_id}/changeset_revision": { + /** + * Create Changeset Revision + * @description upload new revision to the repository + */ + post: operations["repositories__create_revision"] + } + "/api/repositories/{encoded_repository_id}/deprecated": { + /** Set Deprecated */ + put: operations["repositories__set_deprecated"] + /** Unset Deprecated */ + delete: operations["repositories__unset_deprecated"] + } + "/api/repositories/{encoded_repository_id}/metadata": { + /** + * Metadata + * @description Get information about repository metadata + */ + get: operations["repositories__metadata"] + } + "/api/repositories/{encoded_repository_id}/permissions": { + /** Permissions */ + get: operations["repositories__permissions"] + } + "/api/repositories/{encoded_repository_id}/reset_metadata": { + /** + * Reset Metadata On Repository + * @description reset metadata on a repository + */ + post: operations["repositories__reset"] + } + "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious": { + /** Set Malicious */ + put: operations["repositories__set_malicious"] + /** Unset Malicious */ + delete: operations["repositories__unset_malicious"] + } + "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/readmes": { + /** + * Get Readmes + * @description fetch readmes for repository revision + */ + get: operations["repositories__readmes"] + } + "/api/tools": { + /** Index */ + get: operations["tools__index"] + } + "/api/tools/build_search_index": { + /** + * Build Search Index + * @description Not part of the stable API, just something to simplify + * bootstrapping tool sheds, scripting, testing, etc... + */ + put: operations["tools__build_search_index"] + } + "/api/users": { + /** + * Index + * @description index users + */ + get: operations["users__index"] + /** + * Create + * @description create a user + */ + post: operations["users__create"] + } + "/api/users/current": { + /** + * Current + * @description show current user + */ + get: operations["users__current"] + } + "/api/users/{encoded_user_id}": { + /** + * Show + * @description show a user + */ + get: operations["users__show"] + } + "/api/users/{encoded_user_id}/api_key": { + /** Return the user's API key */ + get: operations["users__get_or_create_api_key"] + /** Creates a new API key for the user */ + post: operations["users__create_api_key"] + /** Delete the current API key of the user */ + delete: operations["users__delete_api_key"] + } + "/api/version": { + /** Version */ + get: operations["configuration__version"] + } + "/api_internal/change_password": { + /** + * Change Password + * @description reset a user + */ + put: operations["users__internal_change_password"] + } + "/api_internal/login": { + /** + * Internal Login + * @description login to web UI + */ + put: operations["users__internal_login"] + } + "/api_internal/logout": { + /** + * Internal Logout + * @description logout of web UI + */ + put: operations["users__internal_logout"] + } + "/api_internal/register": { + /** + * Register + * @description register a user + */ + post: operations["users__internal_register"] + } + "/api_internal/repositories/{encoded_repository_id}/metadata": { + /** + * Metadata Internal + * @description Get information about repository metadata + */ + get: operations["repositories__internal_metadata"] + } +} + +export type webhooks = Record + +export interface components { + schemas: { + /** APIKeyResponse */ + APIKeyResponse: { + /** Api Key */ + api_key: string + } + /** Body_repositories__create_revision */ + Body_repositories__create_revision: { + /** Commit Message */ + commit_message?: Record + /** Files */ + files?: string[] + } + /** BuildSearchIndexResponse */ + BuildSearchIndexResponse: { + /** Repositories Indexed */ + repositories_indexed: number + /** Tools Indexed */ + tools_indexed: number + } + /** Category */ + Category: { + /** Description */ + description: string + /** Id */ + id: string + /** Name */ + name: string + /** Repositories */ + repositories: number + } + /** Checksum */ + Checksum: { + /** + * Checksum + * @description The hex-string encoded checksum for the data. + */ + checksum: string + /** + * Type + * @description The digest method used to create the checksum. + * The value (e.g. `sha-256`) SHOULD be listed as `Hash Name String` in the https://github.com/ga4gh-discovery/ga4gh-checksum/blob/master/hash-alg.csv[GA4GH Checksum Hash Algorithm Registry]. + * Other values MAY be used, as long as implementors are aware of the issues discussed in https://tools.ietf.org/html/rfc6920#section-9.4[RFC6920]. + * GA4GH may provide more explicit guidance for use of non-IANA-registered algorithms in the future. + */ + type: string + } + /** CreateCategoryRequest */ + CreateCategoryRequest: { + /** Description */ + description?: string + /** Name */ + name: string + } + /** CreateRepositoryRequest */ + CreateRepositoryRequest: { + /** Category IDs */ + "category_ids[]": string + /** Description */ + description?: string + /** Homepage Url */ + homepage_url?: string + /** Name */ + name: string + /** Remote Repository Url */ + remote_repository_url?: string + /** Synopsis */ + synopsis: string + /** + * Type + * @default unrestricted + * @enum {string} + */ + type?: "repository_suite_definition" | "tool_dependency_definition" | "unrestricted" + } + /** CreateUserRequest */ + CreateUserRequest: { + /** Email */ + email: string + /** Password */ + password: string + /** Username */ + username: string + } + /** + * DescriptorType + * @description An enumeration. + * @enum {unknown} + */ + DescriptorType: "CWL" | "WDL" | "NFL" | "GALAXY" | "SMK" + /** + * DescriptorTypeVersion + * @description The language version for a given descriptor type. The version should correspond to the actual declared version of the descriptor. For example, tools defined in CWL could have a version of `v1.0.2` whereas WDL tools may have a version of `1.0` or `draft-2` + */ + DescriptorTypeVersion: string + /** DetailedRepository */ + DetailedRepository: { + /** Create Time */ + create_time: string + /** Deleted */ + deleted: boolean + /** Deprecated */ + deprecated: boolean + /** Description */ + description: string + /** Homepage Url */ + homepage_url?: string + /** Id */ + id: string + /** Long Description */ + long_description?: string + /** Name */ + name: string + /** Owner */ + owner: string + /** Private */ + private: boolean + /** Remote Repository Url */ + remote_repository_url?: string + /** Times Downloaded */ + times_downloaded: number + /** Type */ + type: string + /** Update Time */ + update_time: string + /** User Id */ + user_id: string + } + /** FailedRepositoryUpdateMessage */ + FailedRepositoryUpdateMessage: { + /** Err Msg */ + err_msg: string + } + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][] + } + /** ImageData */ + ImageData: { + /** + * Checksum + * @description A production (immutable) tool version is required to have a hashcode. Not required otherwise, but might be useful to detect changes. This exposes the hashcode for specific image versions to verify that the container version pulled is actually the version that was indexed by the registry. + * @example [ + * { + * "checksum": "77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182", + * "type": "sha256" + * } + * ] + */ + checksum?: components["schemas"]["Checksum"][] + /** + * Image Name + * @description Used in conjunction with a registry_url if provided to locate images. + * @example [ + * "quay.io/seqware/seqware_full/1.1", + * "ubuntu:latest" + * ] + */ + image_name?: string + image_type?: components["schemas"]["ImageType"] + /** + * Registry Host + * @description A docker registry or a URL to a Singularity registry. Used along with image_name to locate a specific image. + * @example [ + * "registry.hub.docker.com" + * ] + */ + registry_host?: string + /** + * Size + * @description Size of the container in bytes. + */ + size?: number + /** + * Updated + * @description Last time the container was updated. + */ + updated?: string + } + /** + * ImageType + * @description An enumeration. + * @enum {unknown} + */ + ImageType: "Docker" | "Singularity" | "Conda" + /** InstallInfo */ + InstallInfo: { + metadata_info?: components["schemas"]["RepositoryMetadataInstallInfo"] + repo_info?: components["schemas"]["RepositoryExtraInstallInfo"] + } + /** Organization */ + Organization: { + /** + * Name + * @description Name of the organization responsible for the service + * @example My organization + */ + name: string + /** + * Url + * Format: uri + * @description URL of the website of the organization (RFC 3986 format) + * @example https://example.com + */ + url: string + } + /** RepositoriesByCategory */ + RepositoriesByCategory: { + /** Description */ + description: string + /** Id */ + id: string + /** Name */ + name: string + /** Repositories */ + repositories: components["schemas"]["Repository"][] + /** Repository Count */ + repository_count: number + } + /** Repository */ + Repository: { + /** Create Time */ + create_time: string + /** Deleted */ + deleted: boolean + /** Deprecated */ + deprecated: boolean + /** Description */ + description: string + /** Homepage Url */ + homepage_url?: string + /** Id */ + id: string + /** Name */ + name: string + /** Owner */ + owner: string + /** Private */ + private: boolean + /** Remote Repository Url */ + remote_repository_url?: string + /** Times Downloaded */ + times_downloaded: number + /** Type */ + type: string + /** Update Time */ + update_time: string + /** User Id */ + user_id: string + } + /** RepositoryDependency */ + RepositoryDependency: { + /** Changeset Revision */ + changeset_revision: string + /** Downloadable */ + downloadable: boolean + /** Has Repository Dependencies */ + has_repository_dependencies: boolean + /** Id */ + id: string + /** Includes Datatypes */ + includes_datatypes?: boolean + /** Includes Tool Dependencies */ + includes_tool_dependencies?: boolean + /** Includes Tools */ + includes_tools: boolean + /** Includes Tools For Display In Tool Panel */ + includes_tools_for_display_in_tool_panel: boolean + /** Includes Workflows */ + includes_workflows?: boolean + /** Invalid Tools */ + invalid_tools: string[] + /** Malicious */ + malicious: boolean + /** Missing Test Components */ + missing_test_components: boolean + /** Numeric Revision */ + numeric_revision: number + repository: components["schemas"]["Repository"] + /** Repository Dependencies */ + repository_dependencies: components["schemas"]["RepositoryDependency"][] + /** Repository Id */ + repository_id: string + /** Tools */ + tools?: components["schemas"]["RepositoryTool"][] + } + /** RepositoryExtraInstallInfo */ + RepositoryExtraInstallInfo: { + /** Changeset Revision */ + changeset_revision: string + /** Ctx Rev */ + ctx_rev: string + /** Description */ + description: string + /** Name */ + name: string + /** Repository Clone Url */ + repository_clone_url: string + /** Repository Dependencies */ + repository_dependencies?: Record + /** Repository Owner */ + repository_owner: string + } + /** RepositoryMetadata */ + RepositoryMetadata: { + [key: string]: components["schemas"]["RepositoryRevisionMetadata"] | undefined + } + /** RepositoryMetadataInstallInfo */ + RepositoryMetadataInstallInfo: { + /** Changeset Revision */ + changeset_revision: string + /** Downloadable */ + downloadable: boolean + /** Has Repository Dependencies */ + has_repository_dependencies: boolean + /** Id */ + id: string + /** Includes Tools */ + includes_tools: boolean + /** Includes Tools For Display In Tool Panel */ + includes_tools_for_display_in_tool_panel: boolean + /** Malicious */ + malicious: boolean + /** Repository Id */ + repository_id: string + /** Url */ + url: string + /** Valid Tools */ + valid_tools: components["schemas"]["ValidToolDict"][] + } + /** RepositoryPermissions */ + RepositoryPermissions: { + /** Allow Push */ + allow_push: string[] + /** Can Manage */ + can_manage: boolean + /** Can Push */ + can_push: boolean + } + /** RepositoryRevisionMetadata */ + RepositoryRevisionMetadata: { + /** Changeset Revision */ + changeset_revision: string + /** Downloadable */ + downloadable: boolean + /** Has Repository Dependencies */ + has_repository_dependencies: boolean + /** Id */ + id: string + /** Includes Datatypes */ + includes_datatypes?: boolean + /** Includes Tool Dependencies */ + includes_tool_dependencies?: boolean + /** Includes Tools */ + includes_tools: boolean + /** Includes Tools For Display In Tool Panel */ + includes_tools_for_display_in_tool_panel: boolean + /** Includes Workflows */ + includes_workflows?: boolean + /** Invalid Tools */ + invalid_tools: string[] + /** Malicious */ + malicious: boolean + /** Missing Test Components */ + missing_test_components: boolean + /** Numeric Revision */ + numeric_revision: number + repository: components["schemas"]["Repository"] + /** Repository Dependencies */ + repository_dependencies: components["schemas"]["RepositoryDependency"][] + /** Repository Id */ + repository_id: string + /** Tools */ + tools?: components["schemas"]["RepositoryTool"][] + } + /** RepositoryRevisionReadmes */ + RepositoryRevisionReadmes: { + [key: string]: string | undefined + } + /** RepositorySearchHit */ + RepositorySearchHit: { + repository: components["schemas"]["RepositorySearchResult"] + /** Score */ + score: number + } + /** RepositorySearchResult */ + RepositorySearchResult: { + /** Approved */ + approved: boolean + /** Categories */ + categories: string + /** Description */ + description: string + /** Full Last Updated */ + full_last_updated: string + /** Homepage Url */ + homepage_url?: string + /** Id */ + id: string + /** Last Update */ + last_update?: string + /** Long Description */ + long_description?: string + /** Name */ + name: string + /** Remote Repository Url */ + remote_repository_url?: string + /** Repo Lineage */ + repo_lineage: string + /** Repo Owner Username */ + repo_owner_username: string + /** Times Downloaded */ + times_downloaded: number + } + /** RepositorySearchResults */ + RepositorySearchResults: { + /** Hits */ + hits: components["schemas"]["RepositorySearchHit"][] + /** Hostname */ + hostname: string + /** Page */ + page: string + /** Page Size */ + page_size: string + /** Total Results */ + total_results: string + } + /** RepositoryTool */ + RepositoryTool: { + /** Description */ + description: string + /** Guid */ + guid: string + /** Id */ + id: string + /** Name */ + name: string + /** Requirements */ + requirements: Record[] + /** Tool Config */ + tool_config: string + /** Tool Type */ + tool_type: string + /** Version */ + version: string + } + /** RepositoryUpdate */ + RepositoryUpdate: + | components["schemas"]["ValidRepostiroyUpdateMessage"] + | components["schemas"]["FailedRepositoryUpdateMessage"] + /** ResetMetadataOnRepositoryResponse */ + ResetMetadataOnRepositoryResponse: { + /** Repository Status */ + repository_status: string[] + /** Start Time */ + start_time: string + /** Status */ + status: string + /** Stop Time */ + stop_time: string + } + /** Service */ + Service: { + /** + * Contacturl + * Format: uri + * @description URL of the contact for the provider of this service, e.g. a link to a contact form (RFC 3986 format), or an email (RFC 2368 format). + * @example mailto:support@example.com + */ + contactUrl?: string + /** + * Createdat + * Format: date-time + * @description Timestamp describing when the service was first deployed and available (RFC 3339 format) + * @example 2019-06-04T12:58:19Z + */ + createdAt?: string + /** + * Description + * @description Description of the service. Should be human readable and provide information about the service. + * @example This service provides... + */ + description?: string + /** + * Documentationurl + * Format: uri + * @description URL of the documentation of this service (RFC 3986 format). This should help someone learn how to use your service, including any specifics required to access data, e.g. authentication. + * @example https://docs.myservice.example.com + */ + documentationUrl?: string + /** + * Environment + * @description Environment the service is running in. Use this to distinguish between production, development and testing/staging deployments. Suggested values are prod, test, dev, staging. However this is advised and not enforced. + * @example test + */ + environment?: string + /** + * Id + * @description Unique ID of this service. Reverse domain name notation is recommended, though not required. The identifier should attempt to be globally unique so it can be used in downstream aggregator services e.g. Service Registry. + * @example org.ga4gh.myservice + */ + id: string + /** + * Name + * @description Name of this service. Should be human readable. + * @example My project + */ + name: string + /** + * Organization + * @description Organization providing the service + */ + organization: components["schemas"]["Organization"] + type: components["schemas"]["ServiceType"] + /** + * Updatedat + * Format: date-time + * @description Timestamp describing when the service was last updated (RFC 3339 format) + * @example 2019-06-04T12:58:19Z + */ + updatedAt?: string + /** + * Version + * @description Version of the service being described. Semantic versioning is recommended, but other identifiers, such as dates or commit hashes, are also allowed. The version should be changed whenever the service is updated. + * @example 1.0.0 + */ + version: string + } + /** ServiceType */ + ServiceType: { + /** + * Artifact + * @description Name of the API or GA4GH specification implemented. Official GA4GH types should be assigned as part of standards approval process. Custom artifacts are supported. + * @example beacon + */ + artifact: string + /** + * Group + * @description Namespace in reverse domain name format. Use `org.ga4gh` for implementations compliant with official GA4GH specifications. For services with custom APIs not standardized by GA4GH, or implementations diverging from official GA4GH specifications, use a different namespace (e.g. your organization's reverse domain name). + * @example org.ga4gh + */ + group: string + /** + * Version + * @description Version of the API or specification. GA4GH specifications use semantic versioning. + * @example 1.0.0 + */ + version: string + } + /** Tool */ + Tool: { + /** + * Aliases + * @description Support for this parameter is optional for tool registries that support aliases. + * A list of strings that can be used to identify this tool which could be straight up URLs. + * This can be used to expose alternative ids (such as GUIDs) for a tool + * for registries. Can be used to match tools across registries. + */ + aliases?: string[] + /** + * Checker Url + * @description Optional url to the checker tool that will exit successfully if this tool produced the expected result given test data. + */ + checker_url?: string + /** + * Description + * @description The description of the tool. + */ + description?: string + /** + * Has Checker + * @description Whether this tool has a checker tool associated with it. + */ + has_checker?: boolean + /** + * Id + * @description A unique identifier of the tool, scoped to this registry. + * @example 123456 + */ + id: string + /** + * Meta Version + * @description The version of this tool in the registry. Iterates when fields like the description, author, etc. are updated. + */ + meta_version?: string + /** + * Name + * @description The name of the tool. + */ + name?: string + /** + * Organization + * @description The organization that published the image. + */ + organization: string + toolclass: components["schemas"]["ToolClass"] + /** + * Url + * @description The URL for this tool in this registry. + * @example http://agora.broadinstitute.org/tools/123456 + */ + url: string + /** + * Versions + * @description A list of versions for this tool. + */ + versions: components["schemas"]["ToolVersion"][] + } + /** ToolClass */ + ToolClass: { + /** + * Description + * @description A longer explanation of what this class is and what it can accomplish. + */ + description?: string + /** + * Id + * @description The unique identifier for the class. + */ + id?: string + /** + * Name + * @description A short friendly name for the class. + */ + name?: string + } + /** ToolVersion */ + ToolVersion: { + /** + * Author + * @description Contact information for the author of this version of the tool in the registry. (More complex authorship information is handled by the descriptor). + */ + author?: string[] + /** + * Containerfile + * @description Reports if this tool has a containerfile available. (For Docker-based tools, this would indicate the presence of a Dockerfile) + */ + containerfile?: boolean + /** @description The type (or types) of descriptors available. */ + descriptor_type?: components["schemas"]["DescriptorType"][] + /** + * Descriptor Type Version + * @description A map providing information about the language versions used in this tool. The keys should be the same values used in the `descriptor_type` field, and the value should be an array of all the language versions used for the given `descriptor_type`. Depending on the `descriptor_type` (e.g. CWL) multiple version values may be used in a single tool. + * @example { + * "WDL": ["1.0", "1.0"], + * "CWL": ["v1.0.2"], + * "NFL": ["DSL2"] + * } + */ + descriptor_type_version?: { + [key: string]: components["schemas"]["DescriptorTypeVersion"][] | undefined + } + /** + * Id + * @description An identifier of the version of this tool for this particular tool registry. + * @example v1 + */ + id: string + /** + * Images + * @description All known docker images (and versions/hashes) used by this tool. If the tool has to evaluate any of the docker images strings at runtime, those ones cannot be reported here. + */ + images?: components["schemas"]["ImageData"][] + /** + * Included Apps + * @description An array of IDs for the applications that are stored inside this tool. + * @example [ + * "https://bio.tools/tool/mytum.de/SNAP2/1", + * "https://bio.tools/bioexcel_seqqc" + * ] + */ + included_apps?: string[] + /** + * Is Production + * @description This version of a tool is guaranteed to not change over time (for example, a tool built from a tag in git as opposed to a branch). A production quality tool is required to have a checksum + */ + is_production?: boolean + /** + * Meta Version + * @description The version of this tool version in the registry. Iterates when fields like the description, author, etc. are updated. + */ + meta_version?: string + /** + * Name + * @description The name of the version. + */ + name?: string + /** + * Signed + * @description Reports whether this version of the tool has been signed. + */ + signed?: boolean + /** + * Url + * @description The URL for this tool version in this registry. + * @example http://agora.broadinstitute.org/tools/123456/versions/1 + */ + url: string + /** + * Verified + * @description Reports whether this tool has been verified by a specific organization or individual. + */ + verified?: boolean + /** + * Verified Source + * @description Source of metadata that can support a verified tool, such as an email or URL. + */ + verified_source?: string[] + } + /** UiChangePasswordRequest */ + UiChangePasswordRequest: { + /** Current */ + current: string + /** Password */ + password: string + } + /** UiLoginRequest */ + UiLoginRequest: { + /** Login */ + login: string + /** Password */ + password: string + /** Session Csrf Token */ + session_csrf_token: string + } + /** UiLoginResponse */ + UiLoginResponse: Record + /** UiLogoutRequest */ + UiLogoutRequest: { + /** + * Logout All + * @default false + */ + logout_all?: boolean + /** Session Csrf Token */ + session_csrf_token: string + } + /** UiLogoutResponse */ + UiLogoutResponse: Record + /** UiRegisterRequest */ + UiRegisterRequest: { + /** Bear Field */ + bear_field: string + /** Email */ + email: string + /** Password */ + password: string + /** Username */ + username: string + } + /** UiRegisterResponse */ + UiRegisterResponse: { + /** + * Activation Error + * @default false + */ + activation_error?: boolean + /** + * Activation Sent + * @default false + */ + activation_sent?: boolean + /** Contact Email */ + contact_email?: string + /** Email */ + email: string + } + /** User */ + User: { + /** Id */ + id: string + /** Username */ + username: string + } + /** ValidRepostiroyUpdateMessage */ + ValidRepostiroyUpdateMessage: { + /** Message */ + message: string + } + /** ValidToolDict */ + ValidToolDict: { + /** Add To Tool Panel */ + add_to_tool_panel: boolean + /** Description */ + description: string + /** Guid */ + guid: string + /** Id */ + id: string + /** Name */ + name: string + /** Requirements */ + requirements: Record[] + /** Tests */ + tests: Record[] + /** Tool Config */ + tool_config: string + /** Tool Type */ + tool_type: string + /** Version */ + version: string + /** Version String Cmd */ + version_string_cmd: string + } + /** ValidationError */ + ValidationError: { + /** Location */ + loc: string[] + /** Message */ + msg: string + /** Error Type */ + type: string + } + /** Version */ + Version: { + /** + * Api Version + * @default v1 + */ + api_version?: string + /** Version */ + version: string + /** Version Major */ + version_major: string + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} + +export type external = Record + +export interface operations { + authenticate__baseauth: { + /** Returns returns an API key for authenticated user based on BaseAuth headers. */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["APIKeyResponse"] + } + } + } + } + categories__index: { + /** + * Index + * @description index category + */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Category"][] + } + } + } + } + categories__create: { + /** + * Create + * @description create a category + */ + requestBody: { + content: { + "application/json": components["schemas"]["CreateCategoryRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Category"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + categories__repositories: { + /** + * Repositories + * @description display repositories by category + */ + parameters: { + query?: { + installable?: boolean + sort_key?: string + sort_order?: string + page?: number + } + /** @description The encoded database identifier of the category. */ + path: { + encoded_category_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["RepositoriesByCategory"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + tools_trs_service_info: { + /** Service Info */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Service"] + } + } + } + } + tools__trs_tool_classes: { + /** Tool Classes */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ToolClass"][] + } + } + } + } + tools__trs_index: { + /** Trs Index */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + } + } + tools__trs_get: { + /** Trs Get */ + parameters: { + /** @description See also https://ga4gh.github.io/tool-registry-service-schemas/DataModel/#trs-tool-and-trs-tool-version-ids */ + path: { + tool_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Tool"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + tools__trs_get_versions: { + /** Trs Get Versions */ + parameters: { + /** @description See also https://ga4gh.github.io/tool-registry-service-schemas/DataModel/#trs-tool-and-trs-tool-version-ids */ + path: { + tool_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ToolVersion"][] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__index: { + /** + * Index + * @description Get a list of repositories or perform a search. + */ + parameters?: { + query?: { + q?: string + page?: number + page_size?: number + deleted?: boolean + owner?: string + name?: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": + | components["schemas"]["RepositorySearchResults"] + | components["schemas"]["Repository"][] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__create: { + /** + * Create + * @description create a new repository + */ + requestBody: { + content: { + "application/json": components["schemas"]["CreateRepositoryRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Repository"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__get_ordered_installable_revisions: { + /** + * Get Ordered Installable Revisions + * @description Get an ordered list of the repository changeset revisions that are installable + */ + parameters?: { + query?: { + owner?: string + name?: string + tsr_id?: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string[] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__legacy_install_info: { + /** + * Legacy Install Info + * @description Get information used by the install client to install this repository. + */ + parameters: { + /** @description Name of the target repository. */ + /** @description Owner of the target repository. */ + /** @description Changeset of the target repository. */ + query: { + name: string + owner: string + changeset_revision: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record[] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__install_info: { + /** + * Install Info + * @description Get information used by the install client to install this repository. + */ + parameters: { + /** @description Name of the target repository. */ + /** @description Owner of the target repository. */ + /** @description Changeset of the target repository. */ + query: { + name: string + owner: string + changeset_revision: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InstallInfo"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__reset_legacy: { + /** + * Reset Metadata On Repository Legacy + * @description reset metadata on a repository + */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ResetMetadataOnRepositoryResponse"] + } + } + } + } + repositories__update: { + /** Updates */ + parameters: { + query: { + owner?: string + name?: string + changeset_revision: string + hexlify?: boolean + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__show: { + /** Show */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["DetailedRepository"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__show_allow_push: { + /** Show Allow Push */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string[] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__add_allow_push: { + /** Add Allow Push */ + parameters: { + /** @description The encoded database identifier of the repository. */ + /** @description The target username. */ + path: { + encoded_repository_id: string + username: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string[] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__remove_allow_push: { + /** Remove Allow Push */ + parameters: { + /** @description The encoded database identifier of the repository. */ + /** @description The target username. */ + path: { + encoded_repository_id: string + username: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string[] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__create_revision: { + /** + * Create Changeset Revision + * @description upload new revision to the repository + */ + parameters: { + /** @description Set commit message as a query parameter. */ + query?: { + commit_message?: string + } + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + requestBody?: { + content: { + "multipart/form-data": components["schemas"]["Body_repositories__create_revision"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["RepositoryUpdate"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__set_deprecated: { + /** Set Deprecated */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__unset_deprecated: { + /** Unset Deprecated */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__metadata: { + /** + * Metadata + * @description Get information about repository metadata + */ + parameters: { + /** @description Include only downloadable repositories. */ + query?: { + downloadable_only?: boolean + } + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__permissions: { + /** Permissions */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["RepositoryPermissions"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__reset: { + /** + * Reset Metadata On Repository + * @description reset metadata on a repository + */ + parameters: { + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ResetMetadataOnRepositoryResponse"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__set_malicious: { + /** Set Malicious */ + parameters: { + /** @description The encoded database identifier of the repository. */ + /** @description The changeset revision corresponding to the target revision of the target repository. */ + path: { + encoded_repository_id: string + changeset_revision: string + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__unset_malicious: { + /** Unset Malicious */ + parameters: { + /** @description The encoded database identifier of the repository. */ + /** @description The changeset revision corresponding to the target revision of the target repository. */ + path: { + encoded_repository_id: string + changeset_revision: string + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__readmes: { + /** + * Get Readmes + * @description fetch readmes for repository revision + */ + parameters: { + /** @description The encoded database identifier of the repository. */ + /** @description The changeset revision corresponding to the target revision of the target repository. */ + path: { + encoded_repository_id: string + changeset_revision: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["RepositoryRevisionReadmes"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + tools__index: { + /** Index */ + parameters: { + query: { + q: string + page?: number + page_size?: number + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + tools__build_search_index: { + /** + * Build Search Index + * @description Not part of the stable API, just something to simplify + * bootstrapping tool sheds, scripting, testing, etc... + */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["BuildSearchIndexResponse"] + } + } + } + } + users__index: { + /** + * Index + * @description index users + */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["User"][] + } + } + } + } + users__create: { + /** + * Create + * @description create a user + */ + requestBody: { + content: { + "application/json": components["schemas"]["CreateUserRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["User"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__current: { + /** + * Current + * @description show current user + */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["User"] + } + } + } + } + users__show: { + /** + * Show + * @description show a user + */ + parameters: { + /** @description The encoded database identifier of the user. */ + path: { + encoded_user_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["User"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__get_or_create_api_key: { + /** Return the user's API key */ + parameters: { + /** @description The encoded database identifier of the user. */ + path: { + encoded_user_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__create_api_key: { + /** Creates a new API key for the user */ + parameters: { + /** @description The encoded database identifier of the user. */ + path: { + encoded_user_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__delete_api_key: { + /** Delete the current API key of the user */ + parameters: { + /** @description The encoded database identifier of the user. */ + path: { + encoded_user_id: string + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + configuration__version: { + /** Version */ + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Version"] + } + } + } + } + users__internal_change_password: { + /** + * Change Password + * @description reset a user + */ + requestBody: { + content: { + "application/json": components["schemas"]["UiChangePasswordRequest"] + } + } + responses: { + /** @description Successful Response */ + 204: never + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__internal_login: { + /** + * Internal Login + * @description login to web UI + */ + requestBody: { + content: { + "application/json": components["schemas"]["UiLoginRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["UiLoginResponse"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__internal_logout: { + /** + * Internal Logout + * @description logout of web UI + */ + requestBody: { + content: { + "application/json": components["schemas"]["UiLogoutRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["UiLogoutResponse"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + users__internal_register: { + /** + * Register + * @description register a user + */ + requestBody: { + content: { + "application/json": components["schemas"]["UiRegisterRequest"] + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["UiRegisterResponse"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + repositories__internal_metadata: { + /** + * Metadata Internal + * @description Get information about repository metadata + */ + parameters: { + /** @description Include only downloadable repositories. */ + query?: { + downloadable_only?: boolean + } + /** @description The encoded database identifier of the repository. */ + path: { + encoded_repository_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["RepositoryMetadata"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } +} diff --git a/lib/tool_shed/webapp/frontend/src/schema/types.ts b/lib/tool_shed/webapp/frontend/src/schema/types.ts new file mode 100644 index 000000000000..2e328c9cb591 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/schema/types.ts @@ -0,0 +1,5 @@ +import type { components } from "./schema" + +export type Repository = components["schemas"]["Repository"] +export type RevisionMetadata = components["schemas"]["RepositoryRevisionMetadata"] +export type RepositoryTool = components["schemas"]["RepositoryTool"] diff --git a/lib/tool_shed/webapp/frontend/src/shims-vue.d.ts b/lib/tool_shed/webapp/frontend/src/shims-vue.d.ts new file mode 100644 index 000000000000..bae47cae845d --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module "*.vue" { + import { DefineComponent } from "vue" + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/lib/tool_shed/webapp/frontend/src/stores/auth.store.ts b/lib/tool_shed/webapp/frontend/src/stores/auth.store.ts new file mode 100644 index 000000000000..9392f9d60ba4 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/stores/auth.store.ts @@ -0,0 +1,54 @@ +import { defineStore } from "pinia" +import { ensureCookie, notifyOnCatch } from "@/util" +import { getCurrentUser } from "@/apiUtil" + +import { fetcher } from "@/schema" + +const loginFetcher = fetcher.path("/api_internal/login").method("put").create() +const logoutFetcher = fetcher.path("/api_internal/logout").method("put").create() + +export const useAuthStore = defineStore({ + id: "auth", + state: () => ({ + // initialize state from local storage to enable user to stay logged in + user: JSON.parse(localStorage.getItem("user") || "null"), + returnUrl: null, + }), + actions: { + async setup() { + const user = await getCurrentUser() + this.user = user + // store user details and jwt in local storage to keep user logged in between page refreshes + localStorage.setItem("user", user ? JSON.stringify(user) : "null") + }, + async login(username: string, password: string) { + const token = ensureCookie("session_csrf_token") + console.log(token) + loginFetcher({ + login: username, + password: password, + session_csrf_token: token, + }) + .then(async () => { + // We need to do this outside the router to get updated + // cookies and hence csrf token. + window.location.href = "/login_success" + }) + .catch(notifyOnCatch) + }, + async logout() { + const token = ensureCookie("session_csrf_token") + logoutFetcher({ + session_csrf_token: token, + }) + .then(async () => { + this.user = null + localStorage.removeItem("user") + // We need to do this outside the router to get updated + // cookies and hence csrf token. + window.location.href = "/logout_success" + }) + .catch(notifyOnCatch) + }, + }, +}) diff --git a/lib/tool_shed/webapp/frontend/src/stores/categories.store.ts b/lib/tool_shed/webapp/frontend/src/stores/categories.store.ts new file mode 100644 index 000000000000..4038ad835f67 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/stores/categories.store.ts @@ -0,0 +1,33 @@ +import { defineStore } from "pinia" + +import { fetcher, components } from "@/schema" +const categoriesFetcher = fetcher.path("/api/categories").method("get").create() +type Category = components["schemas"]["Category"] + +export const useCategoriesStore = defineStore({ + id: "categories", + state: () => ({ + categories: [] as Category[], + loading: true, + }), + actions: { + async getAll() { + this.loading = true + const { data: categories } = await categoriesFetcher({}) + this.categories = categories + this.loading = false + }, + }, + getters: { + byId(state) { + return (categoryId: string) => { + for (const category of state.categories) { + if (category.id == categoryId) { + return category + } + } + return null + } + }, + }, +}) diff --git a/lib/tool_shed/webapp/frontend/src/stores/index.ts b/lib/tool_shed/webapp/frontend/src/stores/index.ts new file mode 100644 index 000000000000..bb94b71fd9fb --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/stores/index.ts @@ -0,0 +1,4 @@ +export { useAuthStore } from "./auth.store" +export { useCategoriesStore } from "./categories.store" +export { useRepositoryStore } from "./repository.store" +export { useUsersStore } from "./users.store" diff --git a/lib/tool_shed/webapp/frontend/src/stores/repository.store.ts b/lib/tool_shed/webapp/frontend/src/stores/repository.store.ts new file mode 100644 index 000000000000..7d5f7223c12c --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/stores/repository.store.ts @@ -0,0 +1,111 @@ +import { defineStore } from "pinia" + +import { fetcher, components } from "@/schema" +const repositoryFetcher = fetcher.path("/api/repositories/{encoded_repository_id}").method("get").create() +const repositoryMetadataFetcher = fetcher + .path("/api_internal/repositories/{encoded_repository_id}/metadata") + .method("get") + .create() +const repositoryPermissionsFetcher = fetcher + .path("/api/repositories/{encoded_repository_id}/permissions") + .method("get") + .create() +const repositoryPermissionsAdder = fetcher + .path("/api/repositories/{encoded_repository_id}/allow_push/{username}") + .method("post") + .create() +const repositoryPermissionsRemover = fetcher + .path("/api/repositories/{encoded_repository_id}/allow_push/{username}") + .method("delete") + .create() +const repositoryInstallInfoFetcher = fetcher.path("/api/repositories/install_info").method("get").create() + +type DetailedRepository = components["schemas"]["DetailedRepository"] +type InstallInfo = components["schemas"]["InstallInfo"] +type RepositoryMetadata = components["schemas"]["RepositoryMetadata"] +type RepositoryPermissions = components["schemas"]["RepositoryPermissions"] + +export const useRepositoryStore = defineStore({ + id: "repository", + state: () => ({ + repositoryId: null as string | null, + repository: null as DetailedRepository | null, + repositoryMetadata: null as RepositoryMetadata | null, + repositoryInstallInfo: null as InstallInfo | null, + repositoryPermissions: null as RepositoryPermissions | null, + loading: true as boolean, + empty: false as boolean, + }), + actions: { + async allowPush(username: string) { + if (this.repositoryId == null) { + throw Error("Logic problem in repository store") + } + const params = { + encoded_repository_id: this.repositoryId, + username: username, + } + await repositoryPermissionsAdder(params) + const { data: _repositoryPermissions } = await repositoryPermissionsFetcher(params) + this.repositoryPermissions = _repositoryPermissions + }, + async disallowPush(username: string) { + if (this.repositoryId == null) { + throw Error("Logic problem in repository store") + } + const params = { + encoded_repository_id: this.repositoryId, + username: username, + } + await repositoryPermissionsRemover(params) + const { data: _repositoryPermissions } = await repositoryPermissionsFetcher(params) + this.repositoryPermissions = _repositoryPermissions + }, + async setId(repositoryId: string) { + this.repositoryId = repositoryId + this.refresh() + }, + async refresh() { + if (!this.repositoryId) { + return + } + this.loading = true + const params = { encoded_repository_id: this.repositoryId } + const metadataParams = { encoded_repository_id: this.repositoryId, downloadable_only: false } + const [{ data: repository }, { data: repositoryMetadata }] = await Promise.all([ + repositoryFetcher(params), + repositoryMetadataFetcher(metadataParams), + ]) + this.repository = repository + this.repositoryMetadata = repositoryMetadata + let repositoryPermissions = { + can_manage: false, + can_push: false, + allow_push: [] as string[], + } + try { + const { data: _repositoryPermissions } = await repositoryPermissionsFetcher(params) + repositoryPermissions = _repositoryPermissions + this.repositoryPermissions = repositoryPermissions + } catch (e) { + // console.log(e) + } + const latestMetadata = Object.values(repositoryMetadata)[0] + if (!latestMetadata) { + this.empty = true + } else { + if (this.empty) { + this.empty = false + } + const installParams = { + name: repository.name, + owner: repository.owner, + changeset_revision: latestMetadata.changeset_revision, + } + const { data: repositoryInstallInfo } = await repositoryInstallInfoFetcher(installParams) + this.repositoryInstallInfo = repositoryInstallInfo + } + this.loading = false + }, + }, +}) diff --git a/lib/tool_shed/webapp/frontend/src/stores/users.store.ts b/lib/tool_shed/webapp/frontend/src/stores/users.store.ts new file mode 100644 index 000000000000..13cb403f801f --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/stores/users.store.ts @@ -0,0 +1,22 @@ +import { defineStore } from "pinia" + +import { fetcher, components } from "@/schema" +const usersFetcher = fetcher.path("/api/users").method("get").create() + +type User = components["schemas"]["User"] + +export const useUsersStore = defineStore({ + id: "users", + state: () => ({ + users: [] as User[], + loading: true, + }), + actions: { + async getAll() { + this.loading = true + const { data: users } = await usersFetcher({}) + this.users = users + this.loading = false + }, + }, +}) diff --git a/lib/tool_shed/webapp/frontend/src/util.ts b/lib/tool_shed/webapp/frontend/src/util.ts new file mode 100644 index 000000000000..fec1486bd948 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/util.ts @@ -0,0 +1,48 @@ +import { copyToClipboard, Notify, Cookies } from "quasar" +import type { QNotifyCreateOptions } from "quasar" +import { type LocationQueryValue } from "vue-router" +import { ApiError } from "openapi-typescript-fetch" + +export function getCookie(name: string): string | null { + return Cookies.get(name) +} + +export function ensureCookie(name: string): string { + const cookie = getCookie(name) + if (cookie == null) { + notify("An important cookie was not set by the tool shed server, this may result in serious problems.") + throw Error(`Cookie ${name} not set`) + } + return cookie +} + +export function notify(notification: string, type: string | null = null) { + const opts: QNotifyCreateOptions = { + message: notification, + } + if (type) { + opts.type = type + } + Notify.create(opts) +} + +export async function copyAndNotify(value: string, notification: string) { + await copyToClipboard(value) + notify(notification) +} + +export function errorMessage(e: Error): string { + if (e instanceof ApiError) { + return e.data.err_msg + } else { + return JSON.stringify(e) + } +} + +export function queryParamToString(param: LocationQueryValue | LocationQueryValue[]): string | null { + return Array.isArray(param) ? param[0] : param +} + +export function notifyOnCatch(e: Error) { + notify(errorMessage(e)) +} diff --git a/lib/tool_shed/webapp/frontend/src/vite-env.d.ts b/lib/tool_shed/webapp/frontend/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/lib/tool_shed/webapp/frontend/static/favicon.ico b/lib/tool_shed/webapp/frontend/static/favicon.ico new file mode 100644 index 000000000000..cf52fdcad290 Binary files /dev/null and b/lib/tool_shed/webapp/frontend/static/favicon.ico differ diff --git a/lib/tool_shed/webapp/frontend/tsconfig.json b/lib/tool_shed/webapp/frontend/tsconfig.json new file mode 100644 index 000000000000..7dad262e0c00 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "lib": ["esnext", "dom", "dom.iterable", "scripthost"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/lib/tool_shed/webapp/frontend/vite.config.ts b/lib/tool_shed/webapp/frontend/vite.config.ts new file mode 100644 index 000000000000..ccf3705b8553 --- /dev/null +++ b/lib/tool_shed/webapp/frontend/vite.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { quasar, transformAssetUrls } from '@quasar/vite-plugin' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue({ + template: { transformAssetUrls }, + }), + + quasar({ + sassVariables: 'src/quasar-variables.sass', + }), + ], + build: {}, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}) diff --git a/lib/tool_shed/webapp/graphql-schema.json b/lib/tool_shed/webapp/graphql-schema.json new file mode 100644 index 000000000000..d51019220a41 --- /dev/null +++ b/lib/tool_shed/webapp/graphql-schema.json @@ -0,0 +1,2990 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "users", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repositories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepository", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "categories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleCategory", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "revisions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepositoryMetadata", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": null, + "args": [ + { + "name": "id", + "description": "The ID of the object", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relayUsers", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelayUserSortEnum", + "ofType": null + } + }, + "defaultValue": "[ID_ASC]" + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RelayUserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relayRepositoriesForCategory", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelayRepositorySortEnum", + "ofType": null + } + }, + "defaultValue": "[ID_ASC]" + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RelayRepositoryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relayRepositories", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelayRepositorySortEnum", + "ofType": null + } + }, + "defaultValue": "[ID_ASC]" + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RelayRepositoryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relayCategories", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelayCategorySortEnum", + "ofType": null + } + }, + "defaultValue": "[ID_ASC]" + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RelayCategoryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relayRevisions", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelayRepositoryMetadataSortEnum", + "ofType": null + } + }, + "defaultValue": "[ID_ASC]" + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RelayRepositoryMetadataConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SimpleUser", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "username", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SimpleRepository", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "remoteRepositoryUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "homepageUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longDescription", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "categories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleCategory", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "metadataRevisions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepositoryMetadata", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "downloadableRevisions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepositoryMetadata", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateTime", + "description": "The `DateTime` scalar type represents a DateTime\nvalue as specified by\n[iso8601](https://en.wikipedia.org/wiki/ISO_8601).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SimpleCategory", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repositories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepository", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SimpleRepositoryMetadata", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repository", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepository", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "changesetRevision", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "numericRevision", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "malicious", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "downloadable", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "description": "An object with an ID", + "fields": [ + { + "name": "id", + "description": "The ID of the object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "RelayUser", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RelayRepository", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RelayCategory", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryMetadata", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "RelayUserSortEnum", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ID_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ID_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USERNAME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USERNAME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayUserConnection", + "description": null, + "fields": [ + { + "name": "pageInfo", + "description": "Pagination data for this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Contains the nodes in this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RelayUserEdge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayUserEdge", + "description": "A Relay edge containing a `RelayUser` and its cursor.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RelayUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayUser", + "description": null, + "fields": [ + { + "name": "id", + "description": "The ID of the object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "username", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "RelayRepositorySortEnum", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ID_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ID_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREATE_TIME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREATE_TIME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UPDATE_TIME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UPDATE_TIME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NAME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NAME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TYPE_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TYPE_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REMOTE_REPOSITORY_URL_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REMOTE_REPOSITORY_URL_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HOMEPAGE_URL_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HOMEPAGE_URL_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DESCRIPTION_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DESCRIPTION_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LONG_DESCRIPTION_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LONG_DESCRIPTION_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryConnection", + "description": null, + "fields": [ + { + "name": "pageInfo", + "description": "Pagination data for this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Contains the nodes in this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RelayRepositoryEdge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryEdge", + "description": "A Relay edge containing a `RelayRepository` and its cursor.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RelayRepository", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepository", + "description": null, + "fields": [ + { + "name": "id", + "description": "The ID of the object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "remoteRepositoryUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "homepageUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longDescription", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "categories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleCategory", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "RelayCategorySortEnum", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ID_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ID_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREATE_TIME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREATE_TIME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UPDATE_TIME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UPDATE_TIME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NAME_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NAME_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DESCRIPTION_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DESCRIPTION_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DELETED_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DELETED_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayCategoryConnection", + "description": null, + "fields": [ + { + "name": "pageInfo", + "description": "Pagination data for this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Contains the nodes in this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RelayCategoryEdge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayCategoryEdge", + "description": "A Relay edge containing a `RelayCategory` and its cursor.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RelayCategory", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayCategory", + "description": null, + "fields": [ + { + "name": "id", + "description": "The ID of the object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repositories", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepository", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "RelayRepositoryMetadataSortEnum", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ID_ASC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ID_DESC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryMetadataConnection", + "description": null, + "fields": [ + { + "name": "pageInfo", + "description": "Pagination data for this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Contains the nodes in this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RelayRepositoryMetadataEdge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryMetadataEdge", + "description": "A Relay edge containing a `RelayRepositoryMetadata` and its cursor.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RelayRepositoryMetadata", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RelayRepositoryMetadata", + "description": null, + "fields": [ + { + "name": "id", + "description": "The ID of the object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repository", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SimpleRepository", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "changesetRevision", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "numericRevision", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "malicious", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "downloadable", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behaviour of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behaviour of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ] + } + ] + } +} \ No newline at end of file diff --git a/lib/tool_shed/webapp/graphql/__init__.py b/lib/tool_shed/webapp/graphql/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lib/tool_shed/webapp/graphql/schema.py b/lib/tool_shed/webapp/graphql/schema.py new file mode 100644 index 000000000000..af07fb37ebc9 --- /dev/null +++ b/lib/tool_shed/webapp/graphql/schema.py @@ -0,0 +1,244 @@ +import graphene +from graphene import relay +from graphene_sqlalchemy import ( + SQLAlchemyConnectionField, + SQLAlchemyObjectType, +) +from graphene_sqlalchemy.converter import ( + convert_sqlalchemy_hybrid_property_type, + convert_sqlalchemy_type, +) +from graphql import GraphQLResolveInfo +from sqlalchemy.orm import scoped_session +from typing_extensions import TypedDict + +from galaxy.model.custom_types import TrimmedString +from galaxy.security.idencoding import IdEncodingHelper +from tool_shed.webapp.model import ( + Category as SaCategory, + Repository as SaRepository, + RepositoryCategoryAssociation, + RepositoryMetadata as SaRepositoryMetadata, + User as SaUser, +) + +USER_FIELDS = ( + "id", + "username", +) + +CATEGORY_FIELDS = ( + "id", + "create_time", + "update_time", + "name", + "description", + "deleted", +) + +REPOSITORY_FIELDS = ( + "id", + "create_time", + "update_time", + "name", + "type", + "remote_repository_url", + "homepage_url", + "description", + "long_description", +) + +REPOSITORY_METADATA_FIELDS = ( + "id", + "create_time" + "update_time" + "changeset_revision" + "numeric_revision" + "metadata" + "tool_versions" + "malicious" + "downloadable", +) + + +class InfoDict(TypedDict): + session: scoped_session + security: IdEncodingHelper + + +# Map these Galaxy-ism to Graphene for cleaner interfaces. +@convert_sqlalchemy_type.register(TrimmedString) +def convert_sqlalchemy_type_trimmed_string(*args, **kwd): + return graphene.String + + +@convert_sqlalchemy_hybrid_property_type.register(lambda t: t == TrimmedString) +def convert_sqlalchemy_hybrid_property_type_trimmed_string(arg): + return graphene.String + + +class HasIdMixin: + id = graphene.NonNull(graphene.ID) + encoded_id = graphene.NonNull(graphene.String) + + def resolve_encoded_id(self: SQLAlchemyObjectType, info): + return info.context["security"].encode_id(self.id) + + +class UserMixin(HasIdMixin): + username = graphene.NonNull(graphene.String) + + +class RelayUser(SQLAlchemyObjectType, UserMixin): + class Meta: + model = SaUser + only_fields = USER_FIELDS + interfaces = (relay.Node,) + + +class SimpleUser(SQLAlchemyObjectType, UserMixin): + class Meta: + model = SaUser + only_fields = USER_FIELDS + + +class CategoryQueryMixin(HasIdMixin): + name = graphene.NonNull(graphene.String) + repositories = graphene.List(lambda: SimpleRepository) + + def resolve_repositories(self, info: InfoDict): + return [a.repository for a in self.repositories] + + +class SimpleCategory(SQLAlchemyObjectType, CategoryQueryMixin): + class Meta: + model = SaCategory + only_fields = CATEGORY_FIELDS + + +class RelayCategory(SQLAlchemyObjectType, CategoryQueryMixin): + class Meta: + model = SaCategory + only_fields = CATEGORY_FIELDS + interfaces = (relay.Node,) + + +class RepositoryMixin(HasIdMixin): + name = graphene.NonNull(graphene.String) + + +class RelayRepository(SQLAlchemyObjectType, RepositoryMixin): + class Meta: + model = SaRepository + only_fields = REPOSITORY_FIELDS + interfaces = (relay.Node,) + + categories = graphene.List(SimpleCategory) + user = graphene.NonNull(SimpleUser) + + +class RevisionQueryMixin(HasIdMixin): + # I think because it is imperatively mapped, but the fields are not + # auto-populated for this and so we need to be a bit more explicit + create_time = graphene.DateTime() + update_time = graphene.DateTime() + repository = graphene.NonNull(lambda: SimpleRepository) + changeset_revision = graphene.NonNull(graphene.String) + numeric_revision = graphene.Int() + malicious = graphene.Boolean() + downloadable = graphene.Boolean() + + +class SimpleRepositoryMetadata(SQLAlchemyObjectType, RevisionQueryMixin): + class Meta: + model = SaRepositoryMetadata + only_fields = REPOSITORY_METADATA_FIELDS + + +class SimpleRepository(SQLAlchemyObjectType, RepositoryMixin): + class Meta: + model = SaRepository + only_fields = REPOSITORY_FIELDS + + categories = graphene.List(SimpleCategory) + user = graphene.NonNull(SimpleUser) + metadata_revisions = graphene.List(lambda: SimpleRepositoryMetadata) + downloadable_revisions = graphene.List(lambda: SimpleRepositoryMetadata) + + +class RelayRepositoryMetadata(SQLAlchemyObjectType, RevisionQueryMixin): + class Meta: + model = SaRepositoryMetadata + only_fields = REPOSITORY_METADATA_FIELDS + interfaces = (relay.Node,) + + +class RepositoriesForCategoryField(SQLAlchemyConnectionField): + def __init__(self): + super().__init__(RelayRepository.connection, id=graphene.Int(), encoded_id=graphene.String()) + + @classmethod + def get_query(cls, model, info: GraphQLResolveInfo, sort=None, **args): + repository_query = super().get_query(model, info, sort=sort, **args) + context: InfoDict = info.root_value + query_id = args.get("id") + if not query_id: + encoded_id = args.get("encoded_id") + assert encoded_id, f"Invalid encodedId found {encoded_id} in args {args}" + query_id = context["security"].decode_id(encoded_id) + if query_id: + rval = repository_query.join( + RepositoryCategoryAssociation, + SaRepository.id == RepositoryCategoryAssociation.repository_id, + ).filter(RepositoryCategoryAssociation.category_id == query_id) + return rval + else: + return repository_query + + +class RepositoriesForOwnerField(SQLAlchemyConnectionField): + def __init__(self): + super().__init__(RelayRepository.connection, username=graphene.String()) + + @classmethod + def get_query(cls, model, info: GraphQLResolveInfo, sort=None, **args): + repository_query = super().get_query(model, info, sort=sort, **args) + username = args.get("username") + rval = repository_query.join( + SaUser, + ).filter(SaUser.username == username) + return rval + + +class Query(graphene.ObjectType): + users = graphene.List(SimpleUser) + repositories = graphene.List(SimpleRepository) + categories = graphene.List(SimpleCategory) + revisions = graphene.List(SimpleRepositoryMetadata) + + node = relay.Node.Field() + relay_users = SQLAlchemyConnectionField(RelayUser.connection) + relay_repositories_for_category = RepositoriesForCategoryField() + relay_repositories_for_owner = RepositoriesForOwnerField() + relay_repositories = SQLAlchemyConnectionField(RelayRepository.connection) + relay_categories = SQLAlchemyConnectionField(RelayCategory.connection) + relay_revisions = SQLAlchemyConnectionField(RelayRepositoryMetadata.connection) + + def resolve_users(self, info: InfoDict): + query = SimpleUser.get_query(info) + return query.all() + + def resolve_repositories(self, info: InfoDict): + query = SimpleRepository.get_query(info) + return query.all() + + def resolve_categories(self, info: InfoDict): + query = SimpleCategory.get_query(info) + return query.all() + + def resolve_revisions(self, info: InfoDict): + query = SimpleRepositoryMetadata.get_query(info) + return query.all() + + +schema = graphene.Schema(query=Query, types=[SimpleCategory]) diff --git a/lib/tool_shed_client/schema/__init__.py b/lib/tool_shed_client/schema/__init__.py index 91dfd767170c..2e7e1d7a9014 100644 --- a/lib/tool_shed_client/schema/__init__.py +++ b/lib/tool_shed_client/schema/__init__.py @@ -131,7 +131,19 @@ def is_ok(self): class RepositoryTool(BaseModel): - pass + # Added back in post v2 in order for the frontend to render + # tool descriptions on the repository page. + description: str + guid: str + id: str + name: str + requirements: list + tool_config: str + tool_type: str + version: str + # add_to_tool_panel: bool + # tests: list + # version_string_cmd: Optional[str] class RepositoryRevisionMetadata(BaseModel): @@ -139,6 +151,7 @@ class RepositoryRevisionMetadata(BaseModel): repository: Repository repository_dependencies: List["RepositoryDependency"] tools: Optional[List["RepositoryTool"]] + invalid_tools: List[str] # added for rendering list of invalid tools in 2.0 frontend repository_id: str numeric_revision: int changeset_revision: str @@ -240,7 +253,10 @@ class RepositoryIndexRequest(BaseModel): deleted: str = "false" -class RepositoriesByCategory(Category): +class RepositoriesByCategory(BaseModel): + id: str + name: str + description: str repository_count: int repositories: List[Repository] @@ -424,7 +440,7 @@ def from_legacy_dict(as_dict: RepositoryMetadataInstallInfoDict) -> "RepositoryM malicious=as_dict["malicious"], repository_id=as_dict["repository_id"], url=as_dict["url"], - valid_tools=ValidTool.from_legacy_list(as_dict["valid_tools"]), + valid_tools=ValidTool.from_legacy_list(as_dict.get("valid_tools", [])), ) diff --git a/packages/test_driver/setup.cfg b/packages/test_driver/setup.cfg index 699fd1706699..cd64afd37a8c 100644 --- a/packages/test_driver/setup.cfg +++ b/packages/test_driver/setup.cfg @@ -40,6 +40,8 @@ install_requires = galaxy-util galaxy-web-apps pytest + graphene-sqlalchemy==3.0.0b3 # these are only needed by tool shed - which we've split out but the test driver loads + starlette-graphene3 packages = find: python_requires = >=3.7 diff --git a/pyproject.toml b/pyproject.toml index 19571905886c..4794d781629d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ fastapi-utils = "*" fs = "*" future = "*" galaxy_sequence_utils = "*" +graphene-sqlalchemy = "3.0.0b3" # need a beta release to be compat. with starlette plugin gravity = ">=1.0" gunicorn = "*" gxformat2 = "*" @@ -112,6 +113,7 @@ sqlitedict = "*" sqlparse = "*" starlette = "*" starlette-context = "*" +starlette-graphene3 = "*" svgwrite = "*" tifffile = "*" tuswsgi = "*" diff --git a/run_tool_shed.sh b/run_tool_shed.sh index ff88729db125..adff4ba9cdb6 100755 --- a/run_tool_shed.sh +++ b/run_tool_shed.sh @@ -3,6 +3,7 @@ cd "$(dirname "$0")" +export GALAXY_SKIP_CLIENT_BUILD=1 TOOL_SHED_PID=${TOOL_SHED_PID:-tool_shed_webapp.pid} TOOL_SHED_LOG=${TOOL_SHED_LOG:-tool_shed_webapp.log} PID_FILE=$TOOL_SHED_PID diff --git a/scripts/bootstrap_test_shed.py b/scripts/bootstrap_test_shed.py index 40f257ded9e4..c9ab2555cfe3 100644 --- a/scripts/bootstrap_test_shed.py +++ b/scripts/bootstrap_test_shed.py @@ -56,6 +56,7 @@ def main(argv: List[str]) -> None: {"name": "Invalid Test Tools", "description": "A contains a repository with invalid tools."} ) populator.setup_bismark_repo(category_id=category.id) + populator.setup_test_data_repo("0010", category_id=category.id) category = populator.new_category_if_needed({"name": "Test Category", "description": "A longer test description."}) mirror_main_categories(populator) @@ -65,6 +66,7 @@ def main(argv: List[str]) -> None: populator.new_user_if_needed({"email": "alice@alicesdomain.com"}) populator.new_user_if_needed({"email": "thirduser@threeis.com"}) + populator.setup_test_data_repo("column_maker_with_readme", category_id=category.id) populator.setup_column_maker_repo(prefix="bootstrap", category_id=category.id) populator.setup_column_maker_repo(prefix="bootstrap2", category_id=category.id) diff --git a/test/unit/tool_shed/_util.py b/test/unit/tool_shed/_util.py index d57c1ed39d7b..d59991bca0f1 100644 --- a/test/unit/tool_shed/_util.py +++ b/test/unit/tool_shed/_util.py @@ -7,7 +7,11 @@ mkdtemp, NamedTemporaryFile, ) -from typing import Optional +from typing import ( + Any, + Dict, + Optional, +) import tool_shed.repository_registry from galaxy.security.idencoding import IdEncodingHelper @@ -25,10 +29,12 @@ from tool_shed.util.hgweb_config import hgweb_config_manager from tool_shed.util.repository_util import create_repository from tool_shed.webapp.model import ( + Category, mapping, Repository, User, ) +from tool_shed_client.schema import CreateCategoryRequest TEST_DATA_FILES = TEST_DATA_REPO_FILES TEST_HOST = "localhost" @@ -78,9 +84,7 @@ def security_agent(self): return self.model.security_agent -def user_fixture( - app: TestToolShedApp, username: str, password: str = "testpassword", email: Optional[str] = None -) -> User: +def user_fixture(app: ToolShedApp, username: str, password: str = "testpassword", email: Optional[str] = None) -> User: email = email or f"{username}@galaxyproject.org" return create_user( app, @@ -115,10 +119,13 @@ def provides_repositories_fixture( return ProvidesRepositoriesImpl(app, user) -def repository_fixture(app: ToolShedApp, user: User, name: str) -> Repository: +def repository_fixture(app: ToolShedApp, user: User, name: str, category: Optional[Category] = None) -> Repository: type = rt_util.UNRESTRICTED description = f"test repo named {name}" long_description = f"test repo named {name} a longer description" + category_ids = [] + if category: + category_ids.append(app.security.encode_id(category.id)) repository, message = create_repository( app, name, @@ -126,7 +133,7 @@ def repository_fixture(app: ToolShedApp, user: User, name: str) -> Repository: description, long_description, user.id, - category_ids=None, + category_ids=category_ids, remote_repository_url=None, homepage_url=None, ) @@ -177,3 +184,10 @@ def upload_directories_to_repository( def random_name(len: int = 10) -> str: return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(len)) + + +def create_category(provides_repositories: ProvidesRepositoriesContext, create: Dict[str, Any]) -> Category: + from tool_shed.managers.categories import CategoryManager + + request = CreateCategoryRequest(**create) + return CategoryManager(provides_repositories.app).create(provides_repositories, request) diff --git a/test/unit/tool_shed/test_graphql.py b/test/unit/tool_shed/test_graphql.py new file mode 100644 index 000000000000..111dbf6fccd8 --- /dev/null +++ b/test/unit/tool_shed/test_graphql.py @@ -0,0 +1,331 @@ +from typing import ( + Callable, + List, + Optional, + Tuple, +) + +from graphql.execution import ExecutionResult + +from tool_shed.context import ( + ProvidesRepositoriesContext, + ProvidesUserContext, +) +from tool_shed.webapp.graphql.schema import schema +from tool_shed.webapp.model import ( + Category, + Repository, + RepositoryCategoryAssociation, +) +from ._util import ( + create_category, + repository_fixture, + upload_directories_to_repository, + user_fixture, +) + + +def relay_query(query_name: str, params: Optional[str], node_def: str) -> str: + params_call = f"({params})" if params else "" + return f""" +query {{ + {query_name}{params_call} {{ + edges {{ + cursor + node {{ + {node_def} + }} + }} + pageInfo {{ + endCursor + hasNextPage + }} + }} +}} +""" + + +class PageInfo: + def __init__(self, result: dict): + assert "pageInfo" in result + self.info = result["pageInfo"] + + @property + def end_cursor(self) -> str: + return self.info["endCursor"] + + @property + def has_next_page(self) -> bool: + return self.info["hasNextPage"] + + +def relay_result(result: ExecutionResult) -> Tuple[list, PageInfo]: + data = result.data + assert data + data_values = data.values() + query_result = list(data_values)[0] + return query_result["edges"], PageInfo(query_result) + + +QueryExecutor = Callable[[str], ExecutionResult] + + +def query_execution_builder_for_trans(trans: ProvidesRepositoriesContext) -> QueryExecutor: + cv = context_value(trans) + + def e(query: str) -> ExecutionResult: + return schema.execute(query, context_value=cv, root_value=cv) + + return e + + +def context_value(trans: ProvidesUserContext): + return { + "session": trans.app.model.context, + "security": trans.security, + } + + +def test_simple_repositories(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + e = query_execution_builder_for_trans(provides_repositories) + repositories_query = """ + query { + repositories { + id + encodedId + name + categories { + name + } + user { + username + } + } + } + """ + result = e(repositories_query) + _assert_no_errors(result) + repos = _assert_result_data_has_key(result, "repositories") + repository_names = [r["name"] for r in repos] + assert new_repository.name in repository_names + + +def attach_category(provides_repositories: ProvidesRepositoriesContext, repository: Repository, category: Category): + assoc = RepositoryCategoryAssociation( + repository=repository, + category=category, + ) + provides_repositories.sa_session.add(assoc) + provides_repositories.sa_session.flush() + + +def test_relay_repos_by_category(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + name = new_repository.name + category = create_category(provides_repositories, {"name": "test_graphql_relay_categories_1"}) + user = provides_repositories.user + assert user + uc1 = repository_fixture(provides_repositories.app, user, "uc1") + uc2 = repository_fixture(provides_repositories.app, user, "uc2") + + other_user = user_fixture(provides_repositories.app, "otherusernamec") + ouc1 = repository_fixture(provides_repositories.app, other_user, "ouc1") + ouc2 = repository_fixture(provides_repositories.app, other_user, "ouc2") + + category_id = category.id + e = query_execution_builder_for_trans(provides_repositories) + + names = repository_names(e, "relayRepositoriesForCategory", f"id: {category_id}") + assert len(names) == 0 + + encoded_id = provides_repositories.security.encode_id(category_id) + names = repository_names(e, "relayRepositoriesForCategory", f'encodedId: "{encoded_id}"') + assert len(names) == 0 + attach_category(provides_repositories, new_repository, category) + + names = repository_names(e, "relayRepositoriesForCategory", f'encodedId: "{encoded_id}"') + assert len(names) == 1 + assert name in names + + names = repository_names(e, "relayRepositoriesForCategory", f"id: {category_id}") + assert len(names) == 1 + assert name in names + + attach_category(provides_repositories, uc1, category) + attach_category(provides_repositories, ouc1, category) + names = repository_names(e, "relayRepositoriesForCategory", f'encodedId: "{encoded_id}"') + assert len(names) == 3, names + assert "uc1" in names, names + assert "ouc1" in names, names + + category2 = create_category(provides_repositories, {"name": "test_graphql_relay_categories_2"}) + attach_category(provides_repositories, uc2, category2) + attach_category(provides_repositories, ouc2, category2) + + names = repository_names(e, "relayRepositoriesForCategory", f'encodedId: "{encoded_id}"') + assert len(names) == 3, names + assert "uc1" in names, names + assert "ouc1" in names, names + + encoded_id_2 = provides_repositories.security.encode_id(category2.id) + names = repository_names(e, "relayRepositoriesForCategory", f'encodedId: "{encoded_id_2}"') + assert len(names) == 2, names + assert "uc2" in names, names + assert "ouc2" in names, names + + +def test_simple_categories(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + assert provides_repositories.user + + category = create_category(provides_repositories, {"name": "test_graphql"}) + e = query_execution_builder_for_trans(provides_repositories) + result = e( + """ + query { + categories { + name + encodedId + } + } +""" + ) + _assert_no_errors(result) + categories = _assert_result_data_has_key(result, "categories") + category_names = [c["name"] for c in categories] + assert "test_graphql" in category_names + encoded_id = [c["encodedId"] for c in categories if c["name"] == "test_graphql"][0] + assert encoded_id == provides_repositories.security.encode_id(category.id) + + repository_fixture(provides_repositories.app, provides_repositories.user, "foo1", category=category) + result = e( + """ + query { + categories { + name + repositories { + name + } + } + } +""" + ) + _assert_no_errors(result) + categories = _assert_result_data_has_key(result, "categories") + repositories = [c["repositories"] for c in categories if c["name"] == "test_graphql"][0] + assert repositories + repository_names = [r["name"] for r in repositories] + assert "foo1" in repository_names + + +def test_simple_revisions(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + upload_directories_to_repository(provides_repositories, new_repository, "column_maker_with_download_gaps") + e = query_execution_builder_for_trans(provides_repositories) + # (id: "1") + query = """ + query { + revisions { + id + encodedId + createTime + repository { + name + } + changesetRevision + numericRevision + downloadable + } + } +""" + + result = e(query) + _assert_no_errors(result) + + +def test_relay(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + assert provides_repositories.user + repository_fixture(provides_repositories.app, provides_repositories.user, "foo1") + repository_fixture(provides_repositories.app, provides_repositories.user, "f002") + repository_fixture(provides_repositories.app, provides_repositories.user, "cow") + repository_fixture(provides_repositories.app, provides_repositories.user, "u3") + + e = query_execution_builder_for_trans(provides_repositories) + q1 = relay_query("relayRepositories", "sort: NAME_ASC first: 2", "encodedId, name, type, createTime") + result = e(q1) + _assert_no_errors(result) + edges, page_info = relay_result(result) + has_next_page = page_info.has_next_page + assert has_next_page + + last_cursor = edges[-1]["cursor"] + q2 = relay_query("relayRepositories", f'sort: NAME_ASC first: 2 after: "{last_cursor}"', "name, type, createTime") + result = e(q2) + _assert_no_errors(result) + edges, page_info = relay_result(result) + has_next_page = page_info.has_next_page + + +def test_relay_by_owner(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository): + user = provides_repositories.user + assert user + repository_fixture(provides_repositories.app, user, "u1") + repository_fixture(provides_repositories.app, user, "u2") + repository_fixture(provides_repositories.app, user, "u3") + repository_fixture(provides_repositories.app, user, "u4") + other_user = user_fixture(provides_repositories.app, "otherusername") + repository_fixture(provides_repositories.app, other_user, "ou1") + repository_fixture(provides_repositories.app, other_user, "ou2") + repository_fixture(provides_repositories.app, other_user, "ou3") + repository_fixture(provides_repositories.app, other_user, "ou4") + + e = query_execution_builder_for_trans(provides_repositories) + names = repository_names(e, "relayRepositoriesForOwner", f'username: "{user.username}"') + assert "u1" in names + assert "ou1" not in names + + names = repository_names(e, "relayRepositoriesForOwner", 'username: "otherusername"') + assert "ou4" in names + assert "u4" not in names + + +def repository_names(e: QueryExecutor, field: str, base_variables: str) -> List[str]: + edges = walk_relay(e, field, base_variables, "name") + return [e["node"]["name"] for e in edges] + + +def walk_relay(e: QueryExecutor, field: str, base_variables: str, fragment: str): + variables = f"{base_variables} first: 2" + query = relay_query(field, variables, fragment) + result: ExecutionResult = e(query) + _assert_no_errors(result, query) + all_edges, page_info = relay_result(result) + has_next_page = page_info.has_next_page + while has_next_page: + variables = f'{base_variables} first: 2 after: "${page_info.end_cursor}"' + query = relay_query(field, variables, fragment) + result = e(query) + _assert_no_errors(result, query) + these_edges, page_info = relay_result(result) + if len(these_edges) == 0: + # I was using .options instead of .join and such with the queries + # and this would break. The queries are better now anyway, but + # be careful with new queries - there seem to be bugs around this + # potentially + assert not page_info.has_next_page + break + all_edges.extend(these_edges) + has_next_page = page_info.has_next_page + return all_edges + + +def _assert_result_data_has_key(result: ExecutionResult, key: str): + data = result.data + assert data + assert key in data + return data[key] + + +def _assert_no_errors(result: ExecutionResult, query=None): + if result.errors is not None: + message = f"Found unexpected GraphQL errors {str(result.errors)}" + if query is not None: + message = f"{message} in query {query}" + raise AssertionError(message)