Skip to content

Commit 2770eb2

Browse files
open api specs
1 parent db4a6ee commit 2770eb2

File tree

7 files changed

+300
-16
lines changed

7 files changed

+300
-16
lines changed

api/specs/web-server/_projects_crud.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
from simcore_service_webserver._meta import API_VTAG
3333
from simcore_service_webserver.projects._common_models import ProjectPathParams
3434
from simcore_service_webserver.projects._crud_handlers import ProjectCreateParams
35-
from simcore_service_webserver.projects._crud_handlers_models import ProjectListParams
35+
from simcore_service_webserver.projects._crud_handlers_models import (
36+
ProjectListFullSearchParams,
37+
ProjectListParams,
38+
)
3639

3740
router = APIRouter(
3841
prefix=f"/{API_VTAG}",
@@ -137,6 +140,16 @@ async def clone_project(
137140
...
138141

139142

143+
@router.get(
144+
"/projects:search",
145+
response_model=Page[ProjectListItem],
146+
)
147+
async def list_projects_full_search(
148+
_params: Annotated[ProjectListFullSearchParams, Depends()],
149+
):
150+
...
151+
152+
140153
@router.get(
141154
"/projects/{project_id}/inactivity",
142155
response_model=Envelope[GetProjectInactivityResponse],

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,6 +3291,45 @@ paths:
32913291
application/json:
32923292
schema:
32933293
$ref: '#/components/schemas/Envelope_TaskGet_'
3294+
/v0/projects:search:
3295+
get:
3296+
tags:
3297+
- projects
3298+
summary: List Projects Full Search
3299+
operationId: list_projects_full_search
3300+
parameters:
3301+
- required: false
3302+
schema:
3303+
title: Limit
3304+
exclusiveMaximum: true
3305+
minimum: 1
3306+
type: integer
3307+
default: 20
3308+
maximum: 50
3309+
name: limit
3310+
in: query
3311+
- required: false
3312+
schema:
3313+
title: Offset
3314+
minimum: 0
3315+
type: integer
3316+
default: 0
3317+
name: offset
3318+
in: query
3319+
- required: false
3320+
schema:
3321+
title: Text
3322+
maxLength: 100
3323+
type: string
3324+
name: text
3325+
in: query
3326+
responses:
3327+
'200':
3328+
description: Successful Response
3329+
content:
3330+
application/json:
3331+
schema:
3332+
$ref: '#/components/schemas/Page_ProjectListItem_'
32943333
/v0/projects/{project_id}/inactivity:
32953334
get:
32963335
tags:
@@ -9148,18 +9187,6 @@ components:
91489187
title: Userid
91499188
type: string
91509189
description: the user that started the service
9151-
example:
9152-
published_port: 30000
9153-
entrypoint: /the/entry/point/is/here
9154-
service_uuid: 3fa85f64-5717-4562-b3fc-2c963f66afa6
9155-
service_key: simcore/services/comp/itis/sleeper
9156-
service_version: 1.2.3
9157-
service_host: jupyter_E1O2E-LAH
9158-
service_port: 8081
9159-
service_basepath: /x/E1O2E-LAH
9160-
service_state: pending
9161-
service_message: no suitable node (insufficient resources on 1 node)
9162-
user_id: 123
91639190
NodeGetIdle:
91649191
title: NodeGetIdle
91659192
required:
@@ -11601,13 +11628,14 @@ components:
1160111628
ServiceState:
1160211629
title: ServiceState
1160311630
enum:
11631+
- failed
1160411632
- pending
1160511633
- pulling
1160611634
- starting
1160711635
- running
11608-
- complete
11609-
- failed
1161011636
- stopping
11637+
- complete
11638+
- idle
1161111639
description: An enumeration.
1161211640
ServiceType:
1161311641
title: ServiceType

services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
66
"""
77

8-
98
from aiohttp import web
109
from models_library.access_rights import AccessRights
1110
from models_library.api_schemas_webserver._base import OutputSchema
@@ -143,6 +142,28 @@ async def list_projects( # pylint: disable=too-many-arguments
143142
return projects, total_number_projects
144143

145144

145+
async def list_projects_full_search(
146+
app,
147+
*,
148+
user_id: UserID,
149+
product_name: str,
150+
offset: NonNegativeInt,
151+
limit: int,
152+
text: str | None,
153+
) -> tuple[list[ProjectDict], int]:
154+
db = ProjectDBAPI.get_from_app_context(app)
155+
156+
total_number_projects, db_projects = await db.list_projects_full_search(
157+
user_id=user_id,
158+
product_name=product_name,
159+
text=text,
160+
offset=offset,
161+
limit=limit,
162+
)
163+
164+
return db_projects, total_number_projects
165+
166+
146167
async def get_project(
147168
request: web.Request,
148169
user_id: UserID,

services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
ProjectActiveParams,
5959
ProjectCreateHeaders,
6060
ProjectCreateParams,
61+
ProjectListFullSearchParams,
6162
ProjectListWithJsonStrParams,
6263
)
6364
from ._permalink_api import update_or_pop_permalink_in_project
@@ -226,6 +227,40 @@ async def list_projects(request: web.Request):
226227
)
227228

228229

230+
@routes.get(f"/{VTAG}/projects:search", name="list_projects_full_search")
231+
@login_required
232+
@permission_required("project.read")
233+
@_handle_projects_exceptions
234+
async def list_projects_full_search(request: web.Request):
235+
req_ctx = RequestContext.parse_obj(request)
236+
query_params: ProjectListFullSearchParams = parse_request_query_parameters_as(
237+
ProjectListFullSearchParams, request
238+
)
239+
240+
projects, total_number_of_projects = await _crud_api_read.list_projects_full_search(
241+
request.app,
242+
user_id=req_ctx.user_id,
243+
product_name=req_ctx.product_name,
244+
limit=query_params.limit,
245+
offset=query_params.offset,
246+
text=query_params.text,
247+
)
248+
249+
page = Page[ProjectDict].parse_obj(
250+
paginate_data(
251+
chunk=projects,
252+
request_url=request.url,
253+
total=total_number_of_projects,
254+
limit=query_params.limit,
255+
offset=query_params.offset,
256+
)
257+
)
258+
return web.Response(
259+
text=page.json(**RESPONSE_MODEL_POLICY),
260+
content_type=MIMETYPE_APPLICATION_JSON,
261+
)
262+
263+
229264
#
230265
# - Get https://google.aip.dev/131
231266
# - Get active project: Singleton per-session resources https://google.aip.dev/156

services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,19 @@ class Config:
150150

151151
class ProjectActiveParams(BaseModel):
152152
client_session_id: str
153+
154+
155+
class ProjectListFullSearchParams(PageQueryParameters):
156+
text: str | None = Field(
157+
default=None,
158+
description="Multi column full text search, across all folders and workspaces",
159+
max_length=100,
160+
example="My Project",
161+
)
162+
163+
@validator("text", pre=True)
164+
@classmethod
165+
def text_check_empty_string(cls, v):
166+
if not v:
167+
return None
168+
return v

services/web/server/src/simcore_service_webserver/projects/db.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
from simcore_postgres_database.models.projects_to_folders import projects_to_folders
4444
from simcore_postgres_database.models.projects_to_products import projects_to_products
4545
from simcore_postgres_database.models.wallets import wallets
46+
from simcore_postgres_database.models.workspaces_access_rights import (
47+
workspaces_access_rights,
48+
)
4649
from simcore_postgres_database.utils_groups_extra_properties import (
4750
GroupExtraPropertiesRepo,
4851
)
@@ -486,6 +489,168 @@ async def list_projects( # pylint: disable=too-many-arguments
486489
total_number_of_projects,
487490
)
488491

492+
async def list_projects_full_search(
493+
self,
494+
*,
495+
user_id: PositiveInt,
496+
product_name: ProductName,
497+
text: str | None = None,
498+
offset: int | None = 0,
499+
limit: int | None = None,
500+
) -> tuple[int, list[ProjectDict]]:
501+
async with self.engine.acquire() as conn:
502+
user_groups: list[RowProxy] = await self._list_user_groups(conn, user_id)
503+
504+
access_rights_subquery = (
505+
sa.select(
506+
project_to_groups.c.project_uuid,
507+
sa.func.jsonb_object_agg(
508+
project_to_groups.c.gid,
509+
sa.func.jsonb_build_object(
510+
"read",
511+
project_to_groups.c.read,
512+
"write",
513+
project_to_groups.c.write,
514+
"delete",
515+
project_to_groups.c.delete,
516+
),
517+
)
518+
.filter(project_to_groups.c.read)
519+
.label("access_rights"),
520+
).group_by(project_to_groups.c.project_uuid)
521+
).subquery("access_rights_subquery")
522+
523+
workspace_access_rights_subquery = (
524+
sa.select(
525+
workspaces_access_rights.c.workspace_id,
526+
sa.func.jsonb_object_agg(
527+
workspaces_access_rights.c.gid,
528+
sa.func.jsonb_build_object(
529+
"read",
530+
workspaces_access_rights.c.read,
531+
"write",
532+
workspaces_access_rights.c.write,
533+
"delete",
534+
workspaces_access_rights.c.delete,
535+
),
536+
)
537+
.filter(workspaces_access_rights.c.read)
538+
.label("access_rights"),
539+
).group_by(workspaces_access_rights.c.workspace_id)
540+
).subquery("workspace_access_rights_subquery")
541+
542+
private_workspace_query = (
543+
sa.select(
544+
*[
545+
col
546+
for col in projects.columns
547+
if col.name not in ["access_rights"]
548+
],
549+
access_rights_subquery.c.access_rights,
550+
projects_to_products.c.product_name,
551+
projects_to_folders.c.folder_id,
552+
)
553+
.select_from(
554+
projects.join(access_rights_subquery, isouter=True)
555+
.join(projects_to_products)
556+
.join(
557+
projects_to_folders,
558+
(
559+
(projects_to_folders.c.project_uuid == projects.c.uuid)
560+
& (projects_to_folders.c.user_id.is_(None))
561+
),
562+
isouter=True,
563+
)
564+
)
565+
.where(
566+
(
567+
(projects.c.prj_owner == user_id)
568+
| sa.text(
569+
f"jsonb_exists_any(access_rights_subquery.access_rights, {assemble_array_groups(user_groups)})"
570+
)
571+
)
572+
& (projects.c.workspace_id.is_(None))
573+
& (projects_to_products.c.product_name == product_name)
574+
& (projects.c.hidden.is_(False))
575+
& (projects.c.type == ProjectType.STANDARD)
576+
& (
577+
(projects.c.name.ilike(f"%{text}%"))
578+
| (projects.c.description.ilike(f"%{text}%"))
579+
| (projects.c.uuid.ilike(f"%{text}%"))
580+
)
581+
)
582+
)
583+
584+
shared_workspace_query = (
585+
sa.select(
586+
*[
587+
col
588+
for col in projects.columns
589+
if col.name not in ["access_rights"]
590+
],
591+
workspace_access_rights_subquery.c.access_rights,
592+
projects_to_products.c.product_name,
593+
projects_to_folders.c.folder_id,
594+
)
595+
.select_from(
596+
projects.join(
597+
workspace_access_rights_subquery,
598+
projects.c.workspace_id
599+
== workspace_access_rights_subquery.c.workspace_id,
600+
)
601+
.join(projects_to_products)
602+
.join(
603+
projects_to_folders,
604+
(
605+
(projects_to_folders.c.project_uuid == projects.c.uuid)
606+
& (projects_to_folders.c.user_id == user_id)
607+
),
608+
isouter=True,
609+
)
610+
)
611+
.where(
612+
(
613+
sa.text(
614+
f"jsonb_exists_any(workspace_access_rights_subquery.access_rights, {assemble_array_groups(user_groups)})"
615+
)
616+
)
617+
& (projects.c.workspace_id.is_not(None))
618+
& (projects_to_products.c.product_name == product_name)
619+
& (projects.c.hidden.is_(False))
620+
& (projects.c.type == ProjectType.STANDARD)
621+
& (
622+
(projects.c.name.ilike(f"%{text}%"))
623+
| (projects.c.description.ilike(f"%{text}%"))
624+
| (projects.c.uuid.ilike(f"%{text}%"))
625+
)
626+
)
627+
)
628+
629+
combined_query = sa.union_all(
630+
private_workspace_query, shared_workspace_query
631+
)
632+
633+
count_query = sa.select([func.count()]).select_from(combined_query)
634+
total_count = await conn.scalar(count_query)
635+
636+
list_query = combined_query.offset(offset).limit(limit)
637+
result = await conn.execute(list_query)
638+
rows = await result.fetchall() or []
639+
results: list[UserSpecificProjectDataDB] = [
640+
UserSpecificProjectDataDB.from_orm(row) for row in rows
641+
]
642+
643+
# NOTE: Additional adjustments to make it back compatible
644+
list_of_converted_projects: list[ProjectDict] = []
645+
for project in results:
646+
user_email = await self._get_user_email(conn, project.prj_owner)
647+
converted_project = convert_to_schema_names(project.dict(), user_email)
648+
converted_project["tags"] = [] # <-- Tags not needed for now
649+
converted_project["state"] = None
650+
list_of_converted_projects.append(converted_project)
651+
652+
return cast(int, total_count), list_of_converted_projects
653+
489654
async def list_projects_uuids(self, user_id: int) -> list[str]:
490655
async with self.engine.acquire() as conn:
491656
return [

0 commit comments

Comments
 (0)