Skip to content

Commit 133458b

Browse files
✨ First iteration backend for support center (🗃️) (#8212)
1 parent 3472e17 commit 133458b

File tree

28 files changed

+2352
-74
lines changed

28 files changed

+2352
-74
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Helper script to automatically generate OAS
2+
3+
This OAS are the source of truth
4+
"""
5+
6+
# pylint: disable=redefined-outer-name
7+
# pylint: disable=unused-argument
8+
# pylint: disable=unused-variable
9+
# pylint: disable=too-many-arguments
10+
11+
12+
from typing import Annotated
13+
14+
from _common import as_query
15+
from fastapi import APIRouter, Depends, status
16+
from models_library.api_schemas_webserver.conversations import (
17+
ConversationMessagePatch,
18+
ConversationMessageRestGet,
19+
ConversationPatch,
20+
ConversationRestGet,
21+
)
22+
from models_library.generics import Envelope
23+
from models_library.rest_pagination import Page
24+
from simcore_service_webserver._meta import API_VTAG
25+
from simcore_service_webserver.conversations._controller._common import (
26+
ConversationPathParams,
27+
)
28+
from simcore_service_webserver.conversations._controller._conversations_messages_rest import (
29+
_ConversationMessageCreateBodyParams,
30+
_ConversationMessagePathParams,
31+
_ListConversationMessageQueryParams,
32+
)
33+
from simcore_service_webserver.conversations._controller._conversations_rest import (
34+
_ConversationsCreateBodyParams,
35+
_GetConversationsQueryParams,
36+
_ListConversationsQueryParams,
37+
)
38+
39+
router = APIRouter(
40+
prefix=f"/{API_VTAG}",
41+
tags=[
42+
"conversations",
43+
],
44+
)
45+
46+
47+
#
48+
# API entrypoints CONVERSATIONS
49+
#
50+
51+
52+
@router.post(
53+
"/conversations",
54+
response_model=Envelope[ConversationRestGet],
55+
status_code=status.HTTP_201_CREATED,
56+
)
57+
async def create_conversation(
58+
_body: _ConversationsCreateBodyParams,
59+
_query: Annotated[_GetConversationsQueryParams, Depends()],
60+
): ...
61+
62+
63+
@router.get(
64+
"/conversations",
65+
response_model=Page[ConversationRestGet],
66+
)
67+
async def list_conversations(
68+
_query: Annotated[_ListConversationsQueryParams, Depends()],
69+
): ...
70+
71+
72+
@router.put(
73+
"/conversations/{conversation_id}",
74+
response_model=Envelope[ConversationRestGet],
75+
)
76+
async def update_conversation(
77+
_params: Annotated[ConversationPathParams, Depends()],
78+
_body: ConversationPatch,
79+
_query: Annotated[as_query(_GetConversationsQueryParams), Depends()],
80+
): ...
81+
82+
83+
@router.delete(
84+
"/conversations/{conversation_id}",
85+
status_code=status.HTTP_204_NO_CONTENT,
86+
)
87+
async def delete_conversation(
88+
_params: Annotated[ConversationPathParams, Depends()],
89+
_query: Annotated[as_query(_GetConversationsQueryParams), Depends()],
90+
): ...
91+
92+
93+
@router.get(
94+
"/conversations/{conversation_id}",
95+
response_model=Envelope[ConversationRestGet],
96+
)
97+
async def get_conversation(
98+
_params: Annotated[ConversationPathParams, Depends()],
99+
_query: Annotated[as_query(_GetConversationsQueryParams), Depends()],
100+
): ...
101+
102+
103+
#
104+
# API entrypoints CONVERSATION MESSAGES
105+
#
106+
107+
108+
@router.post(
109+
"/conversations/{conversation_id}/messages",
110+
response_model=Envelope[ConversationMessageRestGet],
111+
status_code=status.HTTP_201_CREATED,
112+
)
113+
async def create_conversation_message(
114+
_params: Annotated[ConversationPathParams, Depends()],
115+
_body: _ConversationMessageCreateBodyParams,
116+
): ...
117+
118+
119+
@router.get(
120+
"/conversations/{conversation_id}/messages",
121+
response_model=Page[ConversationMessageRestGet],
122+
)
123+
async def list_conversation_messages(
124+
_params: Annotated[ConversationPathParams, Depends()],
125+
_query: Annotated[as_query(_ListConversationMessageQueryParams), Depends()],
126+
): ...
127+
128+
129+
@router.put(
130+
"/conversations/{conversation_id}/messages/{message_id}",
131+
response_model=Envelope[ConversationMessageRestGet],
132+
)
133+
async def update_conversation_message(
134+
_params: Annotated[_ConversationMessagePathParams, Depends()],
135+
_body: ConversationMessagePatch,
136+
): ...
137+
138+
139+
@router.delete(
140+
"/conversations/{conversation_id}/messages/{message_id}",
141+
status_code=status.HTTP_204_NO_CONTENT,
142+
)
143+
async def delete_conversation_message(
144+
_params: Annotated[_ConversationMessagePathParams, Depends()],
145+
): ...
146+
147+
148+
@router.get(
149+
"/conversations/{conversation_id}/messages/{message_id}",
150+
response_model=Envelope[ConversationMessageRestGet],
151+
)
152+
async def get_conversation_message(
153+
_params: Annotated[_ConversationMessagePathParams, Depends()],
154+
): ...

api/specs/web-server/_projects_conversations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Annotated
1313

1414
from fastapi import APIRouter, Depends, status
15-
from models_library.api_schemas_webserver.projects_conversations import (
15+
from models_library.api_schemas_webserver.conversations import (
1616
ConversationMessageRestGet,
1717
ConversationRestGet,
1818
)

api/specs/web-server/openapi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# core ---
2222
"_auth",
2323
"_auth_api_keys",
24+
"_conversations",
2425
"_groups",
2526
"_tags",
2627
"_tags_groups", # after _tags
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ..projects import ProjectID
1717
from ._base import InputSchema, OutputSchema
1818

19-
### PROJECT CONVERSATION -------------------------------------------------------------------
19+
### CONVERSATION -------------------------------------------------------------------
2020

2121

2222
class ConversationRestGet(OutputSchema):
@@ -28,6 +28,7 @@ class ConversationRestGet(OutputSchema):
2828
type: ConversationType
2929
created: datetime
3030
modified: datetime
31+
extra_context: dict[str, str]
3132

3233
@classmethod
3334
def from_domain_model(cls, domain: ConversationGetDB) -> Self:
@@ -40,14 +41,15 @@ def from_domain_model(cls, domain: ConversationGetDB) -> Self:
4041
type=domain.type,
4142
created=domain.created,
4243
modified=domain.modified,
44+
extra_context=domain.extra_context,
4345
)
4446

4547

4648
class ConversationPatch(InputSchema):
4749
name: str | None = None
4850

4951

50-
### PROJECT CONVERSATION MESSAGES ---------------------------------------------------------------
52+
### CONVERSATION MESSAGES ---------------------------------------------------------------
5153

5254

5355
class ConversationMessageRestGet(OutputSchema):

packages/models-library/src/models_library/conversations.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import Annotated, TypeAlias
3+
from typing import Annotated, Any, TypeAlias
44
from uuid import UUID
55

66
from models_library.groups import GroupID
@@ -23,6 +23,7 @@ class ConversationType(StrAutoEnum):
2323
PROJECT_ANNOTATION = (
2424
auto() # Something like sticky note, can be located anywhere in the pipeline UI
2525
)
26+
SUPPORT = auto() # Support conversation
2627

2728

2829
class ConversationMessageType(StrAutoEnum):
@@ -44,6 +45,7 @@ class ConversationGetDB(BaseModel):
4445
project_uuid: ProjectID | None
4546
user_group_id: GroupID
4647
type: ConversationType
48+
extra_context: dict[str, Any]
4749

4850
# states
4951
created: datetime
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""modify conversations
2+
3+
Revision ID: b566f1b29012
4+
Revises: 5b998370916a
5+
Create Date: 2025-08-14 15:02:54.784186+00:00
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "b566f1b29012"
15+
down_revision = "5b998370916a"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column(
23+
"conversations",
24+
sa.Column(
25+
"extra_context",
26+
postgresql.JSONB(astext_type=sa.Text()),
27+
server_default=sa.text("'{}'::jsonb"),
28+
nullable=False,
29+
),
30+
)
31+
op.add_column(
32+
"products",
33+
sa.Column("support_standard_group_id", sa.BigInteger(), nullable=True),
34+
)
35+
op.create_foreign_key(
36+
"fk_products_support_standard_group_id",
37+
"products",
38+
"groups",
39+
["support_standard_group_id"],
40+
["gid"],
41+
onupdate="CASCADE",
42+
ondelete="SET NULL",
43+
)
44+
45+
op.execute(
46+
"""
47+
ALTER TYPE conversationtype ADD VALUE 'SUPPORT';
48+
"""
49+
)
50+
51+
# ### end Alembic commands ###
52+
53+
54+
def downgrade():
55+
# ### commands auto generated by Alembic - please adjust! ###
56+
op.drop_constraint(
57+
"fk_products_support_standard_group_id", "products", type_="foreignkey"
58+
)
59+
op.drop_column("products", "support_standard_group_id")
60+
op.drop_column("conversations", "extra_context")
61+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/conversations.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import enum
22

33
import sqlalchemy as sa
4-
from sqlalchemy.dialects.postgresql import UUID
4+
from sqlalchemy.dialects.postgresql import JSONB, UUID
55

66
from ._common import RefActions, column_created_datetime, column_modified_datetime
77
from .base import metadata
@@ -12,6 +12,7 @@
1212
class ConversationType(enum.Enum):
1313
PROJECT_STATIC = "PROJECT_STATIC" # Static conversation for the project
1414
PROJECT_ANNOTATION = "PROJECT_ANNOTATION" # Something like sticky note, can be located anywhere in the pipeline UI
15+
SUPPORT = "SUPPORT" # Support conversation
1516

1617

1718
conversations = sa.Table(
@@ -70,6 +71,13 @@ class ConversationType(enum.Enum):
7071
nullable=False,
7172
doc="Product name identifier. If None, then the item is not exposed",
7273
),
74+
sa.Column(
75+
"extra_context",
76+
JSONB,
77+
nullable=False,
78+
server_default=sa.text("'{}'::jsonb"),
79+
doc="Free JSON to store extra context",
80+
),
7381
column_created_datetime(timezone=True),
7482
column_modified_datetime(timezone=True),
7583
)

packages/postgres-database/src/simcore_postgres_database/models/products.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,18 @@ class ProductLoginSettingsDict(TypedDict, total=False):
269269
nullable=True,
270270
doc="Group associated to this product",
271271
),
272+
sa.Column(
273+
"support_standard_group_id",
274+
sa.BigInteger,
275+
sa.ForeignKey(
276+
groups.c.gid,
277+
name="fk_products_support_standard_group_id",
278+
ondelete=RefActions.SET_NULL,
279+
onupdate=RefActions.CASCADE,
280+
),
281+
unique=False,
282+
nullable=True,
283+
doc="Group associated to this product support",
284+
),
272285
sa.PrimaryKeyConstraint("name", name="products_pk"),
273286
)

0 commit comments

Comments
 (0)