Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
80bcf9c
adding folder_id to project resource
matusdrobuliak66 Sep 27, 2024
0958109
fix db query
matusdrobuliak66 Sep 27, 2024
46c6a93
fix db query
matusdrobuliak66 Sep 27, 2024
af72e5d
fix broken test
matusdrobuliak66 Sep 27, 2024
8e748a4
fix broken test
matusdrobuliak66 Sep 27, 2024
1be94a6
Merge branch 'master' into add-folder-id-to-project-resource
matusdrobuliak66 Sep 27, 2024
932ef42
open api specs
matusdrobuliak66 Sep 27, 2024
d41dd40
Merge branch 'add-folder-id-to-project-resource' of github.com:matusd…
matusdrobuliak66 Sep 27, 2024
d07f0ce
fix broken test
matusdrobuliak66 Sep 27, 2024
c01ef1d
fix broken test
matusdrobuliak66 Sep 27, 2024
db4a6ee
review @GitHK
matusdrobuliak66 Sep 27, 2024
2770eb2
open api specs
matusdrobuliak66 Sep 27, 2024
ef741fb
revert test
matusdrobuliak66 Sep 27, 2024
6cd08aa
Merge branch 'master' into project-full-search
matusdrobuliak66 Sep 27, 2024
688b512
review @pcrespov
matusdrobuliak66 Sep 30, 2024
54ea674
adding unit tests
matusdrobuliak66 Sep 30, 2024
0ffb7b7
Merge branch 'master' into project-full-search
matusdrobuliak66 Sep 30, 2024
ad0952c
Merge branch 'master' into project-full-search
matusdrobuliak66 Sep 30, 2024
fd90d72
Merge branch 'master' into project-full-search
matusdrobuliak66 Sep 30, 2024
6305513
Merge branch 'master' into project-full-search
matusdrobuliak66 Sep 30, 2024
8bf13a5
fix duplication
matusdrobuliak66 Sep 30, 2024
b0899fa
Merge branch 'project-full-search' of github.com:matusdrobuliak66/osp…
matusdrobuliak66 Sep 30, 2024
fdbef1c
Merge branch 'master' into project-full-search
odeimaiz Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion api/specs/web-server/_projects_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.projects._common_models import ProjectPathParams
from simcore_service_webserver.projects._crud_handlers import ProjectCreateParams
from simcore_service_webserver.projects._crud_handlers_models import ProjectListParams
from simcore_service_webserver.projects._crud_handlers_models import (
ProjectListFullSearchParams,
ProjectListParams,
)

router = APIRouter(
prefix=f"/{API_VTAG}",
Expand Down Expand Up @@ -137,6 +140,16 @@ async def clone_project(
...


@router.get(
"/projects:search",
response_model=Page[ProjectListItem],
)
async def list_projects_full_search(
_params: Annotated[ProjectListFullSearchParams, Depends()],
):
...


@router.get(
"/projects/{project_id}/inactivity",
response_model=Envelope[GetProjectInactivityResponse],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3291,6 +3291,45 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Envelope_TaskGet_'
/v0/projects:search:
get:
tags:
- projects
summary: List Projects Full Search
operationId: list_projects_full_search
parameters:
- required: false
schema:
title: Limit
exclusiveMaximum: true
minimum: 1
type: integer
default: 20
maximum: 50
name: limit
in: query
- required: false
schema:
title: Offset
minimum: 0
type: integer
default: 0
name: offset
in: query
- required: false
schema:
title: Text
maxLength: 100
type: string
name: text
in: query
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Page_ProjectListItem_'
/v0/projects/{project_id}/inactivity:
get:
tags:
Expand Down Expand Up @@ -9148,18 +9187,6 @@ components:
title: Userid
type: string
description: the user that started the service
example:
published_port: 30000
entrypoint: /the/entry/point/is/here
service_uuid: 3fa85f64-5717-4562-b3fc-2c963f66afa6
service_key: simcore/services/comp/itis/sleeper
service_version: 1.2.3
service_host: jupyter_E1O2E-LAH
service_port: 8081
service_basepath: /x/E1O2E-LAH
service_state: pending
service_message: no suitable node (insufficient resources on 1 node)
user_id: 123
NodeGetIdle:
title: NodeGetIdle
required:
Expand Down Expand Up @@ -11601,13 +11628,14 @@ components:
ServiceState:
title: ServiceState
enum:
- failed
- pending
- pulling
- starting
- running
- complete
- failed
- stopping
- complete
- idle
description: An enumeration.
ServiceType:
title: ServiceType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

"""


from aiohttp import web
from models_library.access_rights import AccessRights
from models_library.api_schemas_webserver._base import OutputSchema
Expand Down Expand Up @@ -143,6 +142,28 @@ async def list_projects( # pylint: disable=too-many-arguments
return projects, total_number_projects


async def list_projects_full_search(
app,
*,
user_id: UserID,
product_name: str,
offset: NonNegativeInt,
limit: int,
text: str | None,
) -> tuple[list[ProjectDict], int]:
db = ProjectDBAPI.get_from_app_context(app)

total_number_projects, db_projects = await db.list_projects_full_search(
user_id=user_id,
product_name=product_name,
text=text,
offset=offset,
limit=limit,
)

return db_projects, total_number_projects


async def get_project(
request: web.Request,
user_id: UserID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
ProjectActiveParams,
ProjectCreateHeaders,
ProjectCreateParams,
ProjectListFullSearchParams,
ProjectListWithJsonStrParams,
)
from ._permalink_api import update_or_pop_permalink_in_project
Expand Down Expand Up @@ -226,6 +227,40 @@ async def list_projects(request: web.Request):
)


@routes.get(f"/{VTAG}/projects:search", name="list_projects_full_search")
@login_required
@permission_required("project.read")
@_handle_projects_exceptions
async def list_projects_full_search(request: web.Request):
req_ctx = RequestContext.parse_obj(request)
query_params: ProjectListFullSearchParams = parse_request_query_parameters_as(
ProjectListFullSearchParams, request
)

projects, total_number_of_projects = await _crud_api_read.list_projects_full_search(
request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
limit=query_params.limit,
offset=query_params.offset,
text=query_params.text,
)

page = Page[ProjectDict].parse_obj(
paginate_data(
chunk=projects,
request_url=request.url,
total=total_number_of_projects,
limit=query_params.limit,
offset=query_params.offset,
)
)
return web.Response(
text=page.json(**RESPONSE_MODEL_POLICY),
content_type=MIMETYPE_APPLICATION_JSON,
)


#
# - Get https://google.aip.dev/131
# - Get active project: Singleton per-session resources https://google.aip.dev/156
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,19 @@ class Config:

class ProjectActiveParams(BaseModel):
client_session_id: str


class ProjectListFullSearchParams(PageQueryParameters):
text: str | None = Field(
default=None,
description="Multi column full text search, across all folders and workspaces",
max_length=100,
example="My Project",
)

@validator("text", pre=True)
@classmethod
def text_check_empty_string(cls, v):
if not v:
return None
return v
165 changes: 165 additions & 0 deletions services/web/server/src/simcore_service_webserver/projects/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
from simcore_postgres_database.models.projects_to_folders import projects_to_folders
from simcore_postgres_database.models.projects_to_products import projects_to_products
from simcore_postgres_database.models.wallets import wallets
from simcore_postgres_database.models.workspaces_access_rights import (
workspaces_access_rights,
)
from simcore_postgres_database.utils_groups_extra_properties import (
GroupExtraPropertiesRepo,
)
Expand Down Expand Up @@ -486,6 +489,168 @@ async def list_projects( # pylint: disable=too-many-arguments
total_number_of_projects,
)

async def list_projects_full_search(
self,
*,
user_id: PositiveInt,
product_name: ProductName,
text: str | None = None,
offset: int | None = 0,
limit: int | None = None,
) -> tuple[int, list[ProjectDict]]:
async with self.engine.acquire() as conn:
user_groups: list[RowProxy] = await self._list_user_groups(conn, user_id)

access_rights_subquery = (
sa.select(
project_to_groups.c.project_uuid,
sa.func.jsonb_object_agg(
project_to_groups.c.gid,
sa.func.jsonb_build_object(
"read",
project_to_groups.c.read,
"write",
project_to_groups.c.write,
"delete",
project_to_groups.c.delete,
),
)
.filter(project_to_groups.c.read)
.label("access_rights"),
).group_by(project_to_groups.c.project_uuid)
).subquery("access_rights_subquery")

workspace_access_rights_subquery = (
sa.select(
workspaces_access_rights.c.workspace_id,
sa.func.jsonb_object_agg(
workspaces_access_rights.c.gid,
sa.func.jsonb_build_object(
"read",
workspaces_access_rights.c.read,
"write",
workspaces_access_rights.c.write,
"delete",
workspaces_access_rights.c.delete,
),
)
.filter(workspaces_access_rights.c.read)
.label("access_rights"),
).group_by(workspaces_access_rights.c.workspace_id)
).subquery("workspace_access_rights_subquery")

private_workspace_query = (
sa.select(
*[
col
for col in projects.columns
if col.name not in ["access_rights"]
],
access_rights_subquery.c.access_rights,
projects_to_products.c.product_name,
projects_to_folders.c.folder_id,
)
.select_from(
projects.join(access_rights_subquery, isouter=True)
.join(projects_to_products)
.join(
projects_to_folders,
(
(projects_to_folders.c.project_uuid == projects.c.uuid)
& (projects_to_folders.c.user_id.is_(None))
),
isouter=True,
)
)
.where(
(
(projects.c.prj_owner == user_id)
| sa.text(
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_groups)})"
)
)
& (projects.c.workspace_id.is_(None))
& (projects_to_products.c.product_name == product_name)
& (projects.c.hidden.is_(False))
& (projects.c.type == ProjectType.STANDARD)
& (
(projects.c.name.ilike(f"%{text}%"))
| (projects.c.description.ilike(f"%{text}%"))
| (projects.c.uuid.ilike(f"%{text}%"))
)
)
)

shared_workspace_query = (
sa.select(
*[
col
for col in projects.columns
if col.name not in ["access_rights"]
],
workspace_access_rights_subquery.c.access_rights,
projects_to_products.c.product_name,
projects_to_folders.c.folder_id,
)
.select_from(
projects.join(
workspace_access_rights_subquery,
projects.c.workspace_id
== workspace_access_rights_subquery.c.workspace_id,
)
.join(projects_to_products)
.join(
projects_to_folders,
(
(projects_to_folders.c.project_uuid == projects.c.uuid)
& (projects_to_folders.c.user_id == user_id)
),
isouter=True,
)
)
.where(
(
sa.text(
f"jsonb_exists_any(workspace_access_rights_subquery.access_rights, {assemble_array_groups(user_groups)})"
)
)
& (projects.c.workspace_id.is_not(None))
& (projects_to_products.c.product_name == product_name)
& (projects.c.hidden.is_(False))
& (projects.c.type == ProjectType.STANDARD)
& (
(projects.c.name.ilike(f"%{text}%"))
| (projects.c.description.ilike(f"%{text}%"))
| (projects.c.uuid.ilike(f"%{text}%"))
)
)
)

combined_query = sa.union_all(
private_workspace_query, shared_workspace_query
)

count_query = sa.select([func.count()]).select_from(combined_query)
total_count = await conn.scalar(count_query)

list_query = combined_query.offset(offset).limit(limit)
result = await conn.execute(list_query)
rows = await result.fetchall() or []
results: list[UserSpecificProjectDataDB] = [
UserSpecificProjectDataDB.from_orm(row) for row in rows
]

# NOTE: Additional adjustments to make it back compatible
list_of_converted_projects: list[ProjectDict] = []
for project in results:
user_email = await self._get_user_email(conn, project.prj_owner)
converted_project = convert_to_schema_names(project.dict(), user_email)
converted_project["tags"] = [] # <-- Tags not needed for now
converted_project["state"] = None
list_of_converted_projects.append(converted_project)

return cast(int, total_count), list_of_converted_projects

async def list_projects_uuids(self, user_id: int) -> list[str]:
async with self.engine.acquire() as conn:
return [
Expand Down
Loading