diff --git a/api/specs/web-server/_projects_comments.py b/api/specs/web-server/_projects_comments.py deleted file mode 100644 index 04ad1f1fa43b..000000000000 --- a/api/specs/web-server/_projects_comments.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Helper script to automatically generate OAS - -This OAS are the source of truth -""" - -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument -# pylint: disable=unused-variable -# pylint: disable=too-many-arguments - -from typing import Literal - -from _common import assert_handler_signature_against_model -from fastapi import APIRouter, status -from models_library.generics import Envelope -from models_library.projects import ProjectID -from models_library.projects_comments import CommentID, ProjectsCommentsAPI -from pydantic import NonNegativeInt -from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.projects._controller.comments_rest import ( - _ProjectCommentsBodyParams, - _ProjectCommentsPathParams, - _ProjectCommentsWithCommentPathParams, -) - -router = APIRouter( - prefix=f"/{API_VTAG}", - tags=[ - "projects", - "comments", - ], -) - - -# -# API entrypoints -# - - -@router.post( - "/projects/{project_uuid}/comments", - response_model=Envelope[dict[Literal["comment_id"], CommentID]], - description="Create a new comment for a specific project. The request body should contain the comment contents and user information.", - status_code=status.HTTP_201_CREATED, - deprecated=True, -) -async def create_project_comment( - project_uuid: ProjectID, body: _ProjectCommentsBodyParams -): ... - - -assert_handler_signature_against_model( - create_project_comment, _ProjectCommentsPathParams -) - - -@router.get( - "/projects/{project_uuid}/comments", - response_model=Envelope[list[ProjectsCommentsAPI]], - description="Retrieve all comments for a specific project.", - deprecated=True, -) -async def list_project_comments( - project_uuid: ProjectID, limit: int = 20, offset: NonNegativeInt = 0 -): ... - - -assert_handler_signature_against_model( - list_project_comments, _ProjectCommentsPathParams -) - - -@router.put( - "/projects/{project_uuid}/comments/{comment_id}", - response_model=Envelope[ProjectsCommentsAPI], - description="Update the contents of a specific comment for a project. The request body should contain the updated comment contents.", - deprecated=True, -) -async def update_project_comment( - project_uuid: ProjectID, - comment_id: CommentID, - body: _ProjectCommentsBodyParams, -): ... - - -assert_handler_signature_against_model( - update_project_comment, _ProjectCommentsWithCommentPathParams -) - - -@router.delete( - "/projects/{project_uuid}/comments/{comment_id}", - description="Delete a specific comment associated with a project.", - status_code=status.HTTP_204_NO_CONTENT, - deprecated=True, -) -async def delete_project_comment(project_uuid: ProjectID, comment_id: CommentID): ... - - -assert_handler_signature_against_model( - delete_project_comment, _ProjectCommentsWithCommentPathParams -) - - -@router.get( - "/projects/{project_uuid}/comments/{comment_id}", - response_model=Envelope[ProjectsCommentsAPI], - description="Retrieve a specific comment by its ID within a project.", - deprecated=True, -) -async def get_project_comment(project_uuid: ProjectID, comment_id: CommentID): ... - - -assert_handler_signature_against_model( - get_project_comment, _ProjectCommentsWithCommentPathParams -) diff --git a/api/specs/web-server/openapi.py b/api/specs/web-server/openapi.py index 2ecc07d19999..effd950b1d7e 100644 --- a/api/specs/web-server/openapi.py +++ b/api/specs/web-server/openapi.py @@ -47,7 +47,6 @@ "_nih_sparc_redirections", "_projects", "_projects_access_rights", - "_projects_comments", "_projects_conversations", "_projects_folders", "_projects_metadata", diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/47dc5c0a138e_preparation_for_osparc_io_migration.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/47dc5c0a138e_preparation_for_osparc_io_migration.py new file mode 100644 index 000000000000..862788810e43 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/47dc5c0a138e_preparation_for_osparc_io_migration.py @@ -0,0 +1,392 @@ +"""preparation for osparc.io migration + +Revision ID: 47dc5c0a138e +Revises: b566f1b29012 +Create Date: 2025-05-19 14:55:15.889813+00:00 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "47dc5c0a138e" +down_revision = "b566f1b29012" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "zzz_resource_tracker_service_runs__osparc_io_archive_202508", + sa.Column("product_name", sa.String(), nullable=False), + sa.Column("service_run_id", sa.String(), nullable=False), + sa.Column("wallet_id", sa.BigInteger(), nullable=True), + sa.Column("wallet_name", sa.String(), nullable=True), + sa.Column("pricing_plan_id", sa.BigInteger(), nullable=True), + sa.Column("pricing_unit_id", sa.BigInteger(), nullable=True), + sa.Column("pricing_unit_cost_id", sa.BigInteger(), nullable=True), + sa.Column("pricing_unit_cost", sa.Numeric(scale=2), nullable=True), + sa.Column("simcore_user_agent", sa.String(), nullable=True), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("user_email", sa.String(), nullable=True), + sa.Column("project_id", sa.String(), nullable=False), + sa.Column("project_name", sa.String(), nullable=False), + sa.Column("node_id", sa.String(), nullable=False), + sa.Column("node_name", sa.String(), nullable=False), + sa.Column("parent_project_id", sa.String(), nullable=False), + sa.Column("root_parent_project_id", sa.String(), nullable=False), + sa.Column("root_parent_project_name", sa.String(), nullable=False), + sa.Column("parent_node_id", sa.String(), nullable=False), + sa.Column("root_parent_node_id", sa.String(), nullable=False), + sa.Column("service_key", sa.String(), nullable=False), + sa.Column("service_version", sa.String(), nullable=False), + sa.Column( + "service_type", + sa.Enum( + "COMPUTATIONAL_SERVICE", + "DYNAMIC_SERVICE", + name="resourcetrackerservicetypeosparciohistory", + ), + nullable=False, + ), + sa.Column( + "service_resources", postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column( + "service_additional_metadata", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + ), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("stopped_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "service_run_status", + sa.Enum( + "RUNNING", + "SUCCESS", + "ERROR", + name="resourcetrackerservicerunstatusosparciohistory", + ), + nullable=False, + ), + sa.Column( + "modified", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("last_heartbeat_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("service_run_status_msg", sa.String(), nullable=True), + sa.Column("missed_heartbeat_counter", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("product_name", "service_run_id"), + ) + op.create_index( + op.f( + "ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_started_at" + ), + "zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ["started_at"], + unique=False, + ) + op.create_index( + op.f( + "ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_wallet_id" + ), + "zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ["wallet_id"], + unique=False, + ) + op.create_index( + op.f("ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_user_id"), + "zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ["user_id"], + unique=False, + ) + op.drop_index("ix_projects_comments_project_uuid", table_name="projects_comments") + op.drop_table("projects_comments") + op.drop_constraint("api_keys_user_id_fkey", "api_keys", type_="foreignkey") + op.create_foreign_key( + "fk_api_keys_to_user_id", + "api_keys", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.drop_constraint("user_confirmation_fkey", "confirmations", type_="foreignkey") + op.create_foreign_key( + "user_confirmation_fkey", + "confirmations", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + # op.alter_column( + # "file_meta_data", + # "user_id", + # existing_type=sa.VARCHAR(), + # type_=sa.BigInteger(), + # nullable=False, + # postgresql_using="user_id::bigint", + # ) + op.execute( + "ALTER TABLE file_meta_data ALTER COLUMN user_id TYPE BIGINT USING user_id::bigint" + ) + op.execute("ALTER TABLE file_meta_data ALTER COLUMN user_id SET NOT NULL") + + op.create_foreign_key( + "fk_file_meta_data_user_id_users", + "file_meta_data", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.drop_constraint("fk_new_folders_to_folders_id", "folders_v2", type_="foreignkey") + op.drop_constraint("fk_new_folders_to_groups_gid", "folders_v2", type_="foreignkey") + op.create_foreign_key( + "fk_new_folders_to_folders_id", + "folders_v2", + "folders_v2", + ["parent_folder_id"], + ["folder_id"], + onupdate="CASCADE", + ) + op.create_foreign_key( + "fk_new_folders_to_groups_gid", + "folders_v2", + "groups", + ["created_by_gid"], + ["gid"], + onupdate="CASCADE", + ondelete="SET NULL", + ) + op.create_foreign_key( + "fk_payments_autorecharge_id_wallets", + "payments_autorecharge", + "wallets", + ["wallet_id"], + ["wallet_id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_payments_methods_to_user_id", + "payments_methods", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_payments_methods_to_wallet_id", + "payments_methods", + "wallets", + ["wallet_id"], + ["wallet_id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_payments_transactions_to_products_name", + "payments_transactions", + "products", + ["product_name"], + ["name"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_payments_transactions_to_user_id", + "payments_transactions", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_payments_transactions_to_wallet_id", + "payments_transactions", + "wallets", + ["wallet_id"], + ["wallet_id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_service_runs_to_product_name", + "resource_tracker_service_runs", + "products", + ["product_name"], + ["name"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.drop_constraint("tokens_user_id_fkey", "tokens", type_="foreignkey") + op.create_foreign_key( + "fk_tokens_to_user_id", + "tokens", + "users", + ["user_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("fk_tokens_to_user_id", "tokens", type_="foreignkey") + op.create_foreign_key("tokens_user_id_fkey", "tokens", "users", ["user_id"], ["id"]) + op.drop_constraint( + "fk_service_runs_to_product_name", + "resource_tracker_service_runs", + type_="foreignkey", + ) + op.drop_constraint( + "fk_payments_transactions_to_wallet_id", + "payments_transactions", + type_="foreignkey", + ) + op.drop_constraint( + "fk_payments_transactions_to_user_id", + "payments_transactions", + type_="foreignkey", + ) + op.drop_constraint( + "fk_payments_transactions_to_products_name", + "payments_transactions", + type_="foreignkey", + ) + op.drop_constraint( + "fk_payments_methods_to_wallet_id", "payments_methods", type_="foreignkey" + ) + op.drop_constraint( + "fk_payments_methods_to_user_id", "payments_methods", type_="foreignkey" + ) + op.drop_constraint( + "fk_payments_autorecharge_id_wallets", + "payments_autorecharge", + type_="foreignkey", + ) + op.drop_constraint("fk_new_folders_to_groups_gid", "folders_v2", type_="foreignkey") + op.drop_constraint("fk_new_folders_to_folders_id", "folders_v2", type_="foreignkey") + op.create_foreign_key( + "fk_new_folders_to_groups_gid", + "folders_v2", + "groups", + ["created_by_gid"], + ["gid"], + ondelete="SET NULL", + ) + op.create_foreign_key( + "fk_new_folders_to_folders_id", + "folders_v2", + "folders_v2", + ["parent_folder_id"], + ["folder_id"], + ) + op.drop_constraint( + "fk_file_meta_data_user_id_users", "file_meta_data", type_="foreignkey" + ) + # op.alter_column( + # "file_meta_data", + # "user_id", + # existing_type=sa.BigInteger(), + # type_=sa.VARCHAR(), + # nullable=True, + # ) + + op.execute( + "ALTER TABLE file_meta_data ALTER COLUMN user_id TYPE VARCHAR USING user_id::varchar" + ) + op.execute("ALTER TABLE file_meta_data ALTER COLUMN user_id DROP NOT NULL") + + op.drop_constraint("user_confirmation_fkey", "confirmations", type_="foreignkey") + op.create_foreign_key( + "user_confirmation_fkey", + "confirmations", + "users", + ["user_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint("fk_api_keys_to_user_id", "api_keys", type_="foreignkey") + op.create_foreign_key( + "api_keys_user_id_fkey", + "api_keys", + "users", + ["user_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_table( + "projects_comments", + sa.Column("comment_id", sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column("project_uuid", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column("contents", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column( + "created", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.Column( + "modified", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.ForeignKeyConstraint( + ["project_uuid"], + ["projects.uuid"], + name="fk_projects_comments_project_uuid", + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + name="fk_projects_comments_user_id", + ondelete="SET NULL", + ), + sa.PrimaryKeyConstraint("comment_id", name="projects_comments_pkey"), + ) + op.create_index( + "ix_projects_comments_project_uuid", + "projects_comments", + ["project_uuid"], + unique=False, + ) + op.drop_index( + op.f("ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_user_id"), + table_name="zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ) + op.drop_index( + op.f( + "ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_wallet_id" + ), + table_name="zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ) + op.drop_index( + op.f( + "ix_zzz_resource_tracker_service_runs__osparc_io_archive_202508_started_at" + ), + table_name="zzz_resource_tracker_service_runs__osparc_io_archive_202508", + ) + op.drop_table("zzz_resource_tracker_service_runs__osparc_io_archive_202508") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py b/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py index 2c3f12eca3ab..9d500bcb55e5 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py @@ -37,7 +37,12 @@ sa.Column( "user_id", sa.BigInteger(), - sa.ForeignKey(users.c.id, ondelete=RefActions.CASCADE), + sa.ForeignKey( + users.c.id, + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, + name="fk_api_keys_to_user_id", + ), nullable=False, doc="Identified user", ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py b/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py index 6fd56e8c8e01..411d7c35c05a 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py @@ -1,10 +1,11 @@ -""" User's confirmations table +"""User's confirmations table - - Keeps a list of tokens to identify an action (registration, invitation, reset, etc) authorized - by link to a a user in the framework - - These tokens have an expiration date defined by configuration +- Keeps a list of tokens to identify an action (registration, invitation, reset, etc) authorized +by link to a a user in the framework +- These tokens have an expiration date defined by configuration """ + import enum import sqlalchemy as sa @@ -61,6 +62,7 @@ class ConfirmationAction(enum.Enum): ["user_id"], [users.c.id], name="user_confirmation_fkey", + onupdate=RefActions.CASCADE, ondelete=RefActions.CASCADE, ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/file_meta_data.py b/packages/postgres-database/src/simcore_postgres_database/models/file_meta_data.py index 9ece039863f2..3ede314b1f74 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/file_meta_data.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/file_meta_data.py @@ -1,4 +1,5 @@ import sqlalchemy as sa +from simcore_postgres_database.models._common import RefActions from .base import metadata @@ -11,7 +12,19 @@ sa.Column("object_name", sa.String()), sa.Column("project_id", sa.String(), index=True), sa.Column("node_id", sa.String()), - sa.Column("user_id", sa.String(), index=True), + sa.Column( + "user_id", + sa.BigInteger(), + sa.ForeignKey( + "users.id", + name="fk_file_meta_data_user_id_users", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + ), + nullable=False, + index=True, + doc="The user id with which the run entry is associated", + ), sa.Column("file_id", sa.String(), primary_key=True), sa.Column("created_at", sa.String()), sa.Column("last_modified", sa.String()), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py b/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py index eebfd2079f80..5d06503f78ef 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py @@ -33,6 +33,7 @@ sa.BigInteger, sa.ForeignKey( "folders_v2.folder_id", + onupdate=RefActions.CASCADE, name="fk_new_folders_to_folders_id", ), nullable=True, @@ -77,6 +78,7 @@ "groups.gid", name="fk_new_folders_to_groups_gid", ondelete=RefActions.SET_NULL, + onupdate=RefActions.CASCADE, ), nullable=True, ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py b/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py index df30251c50ce..b96ffdb2e11c 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py @@ -9,6 +9,7 @@ ) from .base import metadata from .payments_methods import payments_methods +from .wallets import wallets # # NOTE: @@ -29,7 +30,12 @@ sa.Column( "wallet_id", sa.BigInteger, - # NOTE: cannot use foreign-key because it would require a link to wallets table + sa.ForeignKey( + wallets.c.wallet_id, + name="fk_payments_autorecharge_id_wallets", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + ), nullable=False, doc="Wallet associated to the auto-recharge", unique=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/payments_methods.py b/packages/postgres-database/src/simcore_postgres_database/models/payments_methods.py index 3aabc3f992c3..9d9d84ee7410 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/payments_methods.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/payments_methods.py @@ -3,11 +3,13 @@ import sqlalchemy as sa from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, ) from .base import metadata +from .wallets import wallets @enum.unique @@ -41,6 +43,12 @@ class InitPromptAckFlowState(str, enum.Enum): sa.Column( "user_id", sa.BigInteger, + sa.ForeignKey( + "users.id", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_payments_methods_to_user_id", + ), nullable=False, doc="Unique identifier of the user", index=True, @@ -48,6 +56,12 @@ class InitPromptAckFlowState(str, enum.Enum): sa.Column( "wallet_id", sa.BigInteger, + sa.ForeignKey( + wallets.c.wallet_id, + name="fk_payments_methods_to_wallet_id", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + ), nullable=False, doc="Unique identifier to the wallet owned by the user", index=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/payments_transactions.py b/packages/postgres-database/src/simcore_postgres_database/models/payments_transactions.py index 21916b0615b6..f44613690b93 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/payments_transactions.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/payments_transactions.py @@ -4,11 +4,13 @@ from ._common import ( NUMERIC_KWARGS, + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, ) from .base import metadata +from .wallets import wallets @unique @@ -60,12 +62,24 @@ def is_acknowledged(self) -> bool: sa.Column( "product_name", sa.String, + sa.ForeignKey( + "products.name", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_payments_transactions_to_products_name", + ), nullable=False, doc="Product name from which the transaction took place", ), sa.Column( "user_id", sa.BigInteger, + sa.ForeignKey( + "users.id", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_payments_transactions_to_user_id", + ), nullable=False, doc="User unique identifier", index=True, @@ -79,6 +93,12 @@ def is_acknowledged(self) -> bool: sa.Column( "wallet_id", sa.BigInteger, + sa.ForeignKey( + wallets.c.wallet_id, + name="fk_payments_transactions_to_wallet_id", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + ), nullable=False, doc="Wallet identifier owned by the user", index=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py deleted file mode 100644 index 919b143bff3e..000000000000 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py +++ /dev/null @@ -1,53 +0,0 @@ -import sqlalchemy as sa - -from ._common import RefActions, column_created_datetime, column_modified_datetime -from .base import metadata -from .projects import projects -from .users import users - -projects_comments = sa.Table( - "projects_comments", - metadata, - sa.Column( - "comment_id", - sa.BigInteger, - nullable=False, - autoincrement=True, - primary_key=True, - doc="Primary key, identifies the comment", - ), - sa.Column( - "project_uuid", - sa.String, - sa.ForeignKey( - projects.c.uuid, - name="fk_projects_comments_project_uuid", - ondelete=RefActions.CASCADE, - onupdate=RefActions.CASCADE, - ), - index=True, - nullable=False, - doc="project reference for this table", - ), - # NOTE: if the user gets deleted, it sets to null which should be interpreted as "unknown" user - sa.Column( - "user_id", - sa.BigInteger, - sa.ForeignKey( - users.c.id, - name="fk_projects_comments_user_id", - ondelete=RefActions.SET_NULL, - ), - doc="user who created the comment", - nullable=True, - ), - sa.Column( - "contents", - sa.String, - nullable=False, - doc="Content of the comment", - ), - column_created_datetime(timezone=True), - column_modified_datetime(timezone=True), - sa.PrimaryKeyConstraint("comment_id", name="projects_comments_pkey"), -) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py index 33eddcb9fc77..0854fa4dd036 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py @@ -1,11 +1,11 @@ -""" resource_tracker_service_runs table -""" +"""resource_tracker_service_runs table""" + import enum import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB -from ._common import NUMERIC_KWARGS, column_modified_datetime +from ._common import NUMERIC_KWARGS, RefActions, column_modified_datetime from .base import metadata @@ -25,7 +25,17 @@ class ResourceTrackerServiceRunStatus(str, enum.Enum): metadata, # Primary keys sa.Column( - "product_name", sa.String, nullable=False, doc="Product name", primary_key=True + "product_name", + sa.String, + sa.ForeignKey( + "products.name", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_service_runs_to_product_name", + ), + nullable=False, + doc="Product name", + primary_key=True, ), sa.Column( "service_run_id", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/tokens.py b/packages/postgres-database/src/simcore_postgres_database/models/tokens.py index 990de23c7247..5a504b6a81b4 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/tokens.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/tokens.py @@ -1,6 +1,7 @@ -""" User Tokens table -""" +"""User Tokens table""" + import sqlalchemy as sa +from simcore_postgres_database.models._common import RefActions from .base import metadata from .users import users @@ -10,7 +11,17 @@ "tokens", metadata, sa.Column("token_id", sa.BigInteger, nullable=False, primary_key=True), - sa.Column("user_id", sa.BigInteger, sa.ForeignKey(users.c.id), nullable=False), + sa.Column( + "user_id", + sa.BigInteger, + sa.ForeignKey( + users.c.id, + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_tokens_to_user_id", + ), + nullable=False, + ), sa.Column("token_service", sa.String, nullable=False), sa.Column("token_data", sa.JSON, nullable=False), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/zzz_resource_tracker_service_runs__osparc_io_archive_202508.py b/packages/postgres-database/src/simcore_postgres_database/models/zzz_resource_tracker_service_runs__osparc_io_archive_202508.py new file mode 100644 index 000000000000..a11538339044 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/zzz_resource_tracker_service_runs__osparc_io_archive_202508.py @@ -0,0 +1,228 @@ +"""resource_tracker_service_runs table""" + +import enum + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +from ._common import NUMERIC_KWARGS, column_modified_datetime +from .base import metadata + + +class ResourceTrackerServiceTypeOsparcIoHistory(str, enum.Enum): + COMPUTATIONAL_SERVICE = "COMPUTATIONAL_SERVICE" + DYNAMIC_SERVICE = "DYNAMIC_SERVICE" + + +class ResourceTrackerServiceRunStatusOsparcIoHistory(str, enum.Enum): + RUNNING = "RUNNING" + SUCCESS = "SUCCESS" + ERROR = "ERROR" + + +zzz_resource_tracker_service_runs__osparc_io_archive_202508 = sa.Table( + "zzz_resource_tracker_service_runs__osparc_io_archive_202508", + metadata, + # Primary keys + sa.Column( + "product_name", sa.String, nullable=False, doc="Product name", primary_key=True + ), + sa.Column( + "service_run_id", + sa.String, + nullable=False, + doc="Refers to the unique service_run_id provided by the director-v2/dynamic-sidecars.", + primary_key=True, + ), + # Wallet fields + sa.Column( + "wallet_id", + sa.BigInteger, + nullable=True, + doc="We want to store the wallet id for tracking/billing purposes and be sure it stays there even when the wallet is deleted (that's also reason why we do not introduce foreign key)", + index=True, + ), + sa.Column( + "wallet_name", + sa.String, + nullable=True, + doc="We want to store the wallet name for tracking/billing purposes and be sure it stays there even when the wallet is deleted (that's also reason why we do not introduce foreign key)", + ), + # Pricing fields + sa.Column( + "pricing_plan_id", + sa.BigInteger, + nullable=True, + doc="Pricing plan id for billing purposes", + ), + sa.Column( + "pricing_unit_id", + sa.BigInteger, + nullable=True, + doc="Pricing unit id for billing purposes", + ), + sa.Column( + "pricing_unit_cost_id", + sa.BigInteger, + nullable=True, + doc="Pricing unit cost id for billing purposes", + ), + sa.Column( + "pricing_unit_cost", + sa.Numeric(**NUMERIC_KWARGS), # type: ignore + nullable=True, + doc="Pricing unit cost used for billing purposes", + ), + # User agent field + sa.Column( + "simcore_user_agent", + sa.String, + nullable=True, + doc="Information about whether it is Puppeteer or not", + ), + # User fields + sa.Column( + "user_id", + sa.BigInteger, + nullable=False, + doc="We want to store the user id for tracking/billing purposes and be sure it stays there even when the user is deleted (that's also reason why we do not introduce foreign key)", + index=True, + ), + sa.Column( + "user_email", + sa.String, + nullable=True, + doc="we want to store the email for tracking/billing purposes and be sure it stays there even when the user is deleted (that's also reason why we do not introduce foreign key)", + ), + # Project fields + sa.Column( + "project_id", # UUID + sa.String, + nullable=False, + doc="We want to store the project id for tracking/billing purposes and be sure it stays there even when the project is deleted (that's also reason why we do not introduce foreign key)", + ), + sa.Column( + "project_name", + sa.String, + nullable=False, + doc="we want to store the project name for tracking/billing purposes and be sure it stays there even when the project is deleted (that's also reason why we do not introduce foreign key)", + ), + # Node fields + sa.Column( + "node_id", # UUID + sa.String, + nullable=False, + doc="We want to store the node id for tracking/billing purposes and be sure it stays there even when the node is deleted (that's also reason why we do not introduce foreign key)", + ), + sa.Column( + "node_name", + sa.String, + nullable=False, + doc="we want to store the node/service name/label for tracking/billing purposes and be sure it stays there even when the node is deleted.", + ), + # Project/Node parent fields + sa.Column( + "parent_project_id", # UUID + sa.String, + nullable=False, + doc="If a user starts computational jobs via a dynamic service, a new project is created in the backend. This newly created project is considered a child project, and the project from which it was created is the parent project. We want to store the parent project ID for tracking and billing purposes, and ensure it remains even when the node is deleted. This is also the reason why we do not introduce a foreign key.", + ), + sa.Column( + "root_parent_project_id", # UUID + sa.String, + nullable=False, + doc="Similar to the parent project concept, we are flexible enough to allow multiple nested computational jobs, which create multiple nested projects. For this reason, we keep the parent project ID, so we know from which project the user started their computation.", + ), + sa.Column( + "root_parent_project_name", + sa.String, + nullable=False, + doc="We want to store the root parent project name for tracking/billing purposes.", + ), + sa.Column( + "parent_node_id", # UUID + sa.String, + nullable=False, + doc="Since each project can have multiple nodes, similar to the parent project concept, we also store the parent node..", + ), + sa.Column( + "root_parent_node_id", # UUID + sa.String, + nullable=False, + doc="Since each project can have multiple nodes, similar to the root parent project concept, we also store the root parent node.", + ), + # Service fields + sa.Column( + "service_key", + sa.String, + nullable=False, + doc="Service Key", + ), + sa.Column( + "service_version", + sa.String, + nullable=False, + doc="Service Version", + ), + sa.Column( + "service_type", + sa.Enum(ResourceTrackerServiceTypeOsparcIoHistory), + nullable=False, + doc="Service type, ex. COMPUTATIONAL, DYNAMIC", + ), + sa.Column( + "service_resources", + JSONB, + nullable=False, + default="'{}'::jsonb", + doc="Service aresources, ex. cpu, gpu, memory, ...", + ), + sa.Column( + "service_additional_metadata", + JSONB, + nullable=False, + default="'{}'::jsonb", + doc="Service additional metadata.", + ), + # Run timestamps + sa.Column( + "started_at", + sa.DateTime(timezone=True), + nullable=False, + doc="Timestamp when the service was started", + index=True, + ), + sa.Column( + "stopped_at", + sa.DateTime(timezone=True), + nullable=True, + doc="Timestamp when the service was stopped", + ), + # Run status + sa.Column( + "service_run_status", # Partial index was defined bellow + sa.Enum(ResourceTrackerServiceRunStatusOsparcIoHistory), + nullable=False, + ), + column_modified_datetime(timezone=True), + # Last Heartbeat + sa.Column( + "last_heartbeat_at", + sa.DateTime(timezone=True), + nullable=False, + doc="Timestamp when was the last heartbeat", + ), + sa.Column( + "service_run_status_msg", + sa.String, + nullable=True, + doc="Custom message/comment, for example to help understand root cause of the error during investigation", + ), + sa.Column( + "missed_heartbeat_counter", + sa.SmallInteger, + nullable=False, + default=0, + doc="How many heartbeat checks have been missed", + ), +) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index c4bef8dbf213..e401b152a941 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -5482,172 +5482,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_ProjectGroupGet__' - /v0/projects/{project_uuid}/comments: - post: - tags: - - projects - - comments - summary: Create Project Comment - description: Create a new comment for a specific project. The request body should - contain the comment contents and user information. - operationId: create_project_comment - deprecated: true - parameters: - - name: project_uuid - in: path - required: true - schema: - type: string - format: uuid - title: Project Uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/_ProjectCommentsBodyParams' - responses: - '201': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_dict_Literal__comment_id____Annotated_int__Gt___' - get: - tags: - - projects - - comments - summary: List Project Comments - description: Retrieve all comments for a specific project. - operationId: list_project_comments - deprecated: true - parameters: - - name: project_uuid - in: path - required: true - schema: - type: string - format: uuid - title: Project Uuid - - name: limit - in: query - required: false - schema: - type: integer - default: 20 - title: Limit - - name: offset - in: query - required: false - schema: - type: integer - minimum: 0 - default: 0 - title: Offset - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_list_ProjectsCommentsAPI__' - /v0/projects/{project_uuid}/comments/{comment_id}: - put: - tags: - - projects - - comments - summary: Update Project Comment - description: Update the contents of a specific comment for a project. The request - body should contain the updated comment contents. - operationId: update_project_comment - deprecated: true - parameters: - - name: project_uuid - in: path - required: true - schema: - type: string - format: uuid - title: Project Uuid - - name: comment_id - in: path - required: true - schema: - type: integer - exclusiveMinimum: true - title: Comment Id - minimum: 0 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/_ProjectCommentsBodyParams' - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_ProjectsCommentsAPI_' - delete: - tags: - - projects - - comments - summary: Delete Project Comment - description: Delete a specific comment associated with a project. - operationId: delete_project_comment - deprecated: true - parameters: - - name: project_uuid - in: path - required: true - schema: - type: string - format: uuid - title: Project Uuid - - name: comment_id - in: path - required: true - schema: - type: integer - exclusiveMinimum: true - title: Comment Id - minimum: 0 - responses: - '204': - description: Successful Response - get: - tags: - - projects - - comments - summary: Get Project Comment - description: Retrieve a specific comment by its ID within a project. - operationId: get_project_comment - deprecated: true - parameters: - - name: project_uuid - in: path - required: true - schema: - type: string - format: uuid - title: Project Uuid - - name: comment_id - in: path - required: true - schema: - type: integer - exclusiveMinimum: true - title: Comment Id - minimum: 0 - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_ProjectsCommentsAPI_' /v0/projects/{project_id}/conversations: post: tags: @@ -11323,19 +11157,6 @@ components: title: Error type: object title: Envelope[ProjectStateOutputSchema] - Envelope_ProjectsCommentsAPI_: - properties: - data: - anyOf: - - $ref: '#/components/schemas/ProjectsCommentsAPI' - - type: 'null' - error: - anyOf: - - {} - - type: 'null' - title: Error - type: object - title: Envelope[ProjectsCommentsAPI] Envelope_RegisterPhoneNextPage_: properties: data: @@ -11646,26 +11467,6 @@ components: title: Error type: object title: Envelope[dict[Annotated[str, StringConstraints], ImageResources]] - Envelope_dict_Literal__comment_id____Annotated_int__Gt___: - properties: - data: - anyOf: - - additionalProperties: - type: integer - exclusiveMinimum: true - minimum: 0 - propertyNames: - const: comment_id - type: object - - type: 'null' - title: Data - error: - anyOf: - - {} - - type: 'null' - title: Error - type: object - title: Envelope[dict[Literal['comment_id'], Annotated[int, Gt]]] Envelope_dict_UUID__Activity__: properties: data: @@ -11968,22 +11769,6 @@ components: title: Error type: object title: Envelope[list[ProjectMetadataPortGet]] - Envelope_list_ProjectsCommentsAPI__: - properties: - data: - anyOf: - - items: - $ref: '#/components/schemas/ProjectsCommentsAPI' - type: array - - type: 'null' - title: Data - error: - anyOf: - - {} - - type: 'null' - title: Error - type: object - title: Envelope[list[ProjectsCommentsAPI]] Envelope_list_ResourceHit__: properties: data: @@ -16343,49 +16128,6 @@ components: - template - user title: ProjectTypeAPI - ProjectsCommentsAPI: - properties: - comment_id: - type: integer - exclusiveMinimum: true - title: Comment Id - description: Primary key, identifies the comment - minimum: 0 - project_uuid: - type: string - format: uuid - title: Project Uuid - description: project reference for this table - user_id: - type: integer - exclusiveMinimum: true - title: User Id - description: user reference for this table - minimum: 0 - contents: - type: string - title: Contents - description: Contents of the comment - created: - type: string - format: date-time - title: Created - description: Timestamp on creation - modified: - type: string - format: date-time - title: Modified - description: Timestamp with last update - additionalProperties: false - type: object - required: - - comment_id - - project_uuid - - user_id - - contents - - created - - modified - title: ProjectsCommentsAPI ProjectsGroupsBodyParams: properties: read: @@ -19063,16 +18805,6 @@ components: title: Expiration 2Fa type: object title: _PageParams - _ProjectCommentsBodyParams: - properties: - contents: - type: string - title: Contents - additionalProperties: false - type: object - required: - - contents - title: _ProjectCommentsBodyParams _ProjectConversationMessagesCreateBodyParams: properties: content: diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_repository.py b/services/web/server/src/simcore_service_webserver/projects/_comments_repository.py deleted file mode 100644 index 1a871f12ed38..000000000000 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_repository.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging - -from aiopg.sa.result import ResultProxy -from models_library.projects import ProjectID -from models_library.projects_comments import CommentID, ProjectsCommentsDB -from models_library.users import UserID -from pydantic import TypeAdapter -from pydantic.types import PositiveInt -from simcore_postgres_database.models.projects_comments import projects_comments -from sqlalchemy import func, literal_column -from sqlalchemy.sql import select - -_logger = logging.getLogger(__name__) - - -async def create_project_comment( - conn, project_uuid: ProjectID, user_id: UserID, contents: str -) -> CommentID: - project_comment_id: ResultProxy = await conn.execute( - projects_comments.insert() - .values( - project_uuid=project_uuid, - user_id=user_id, - contents=contents, - modified=func.now(), - ) - .returning(projects_comments.c.comment_id) - ) - result: tuple[PositiveInt] = await project_comment_id.first() - return TypeAdapter(CommentID).validate_python(result[0]) - - -async def list_project_comments( - conn, - project_uuid: ProjectID, - offset: PositiveInt, - limit: int, -) -> list[ProjectsCommentsDB]: - result = [] - project_comment_result: ResultProxy = await conn.execute( - projects_comments.select() - .where(projects_comments.c.project_uuid == f"{project_uuid}") - .order_by(projects_comments.c.created.asc()) - .offset(offset) - .limit(limit) - ) - result = [ - ProjectsCommentsDB.model_validate(row) - for row in await project_comment_result.fetchall() - ] - return result - - -async def total_project_comments( - conn, - project_uuid: ProjectID, -) -> PositiveInt: - project_comment_result: ResultProxy = await conn.execute( - select(func.count()) - .select_from(projects_comments) - .where(projects_comments.c.project_uuid == f"{project_uuid}") - ) - result: tuple[PositiveInt] = await project_comment_result.first() - return result[0] - - -async def update_project_comment( - conn, - comment_id: CommentID, - project_uuid: ProjectID, - contents: str, -) -> ProjectsCommentsDB: - project_comment_result = await conn.execute( - projects_comments.update() - .values( - project_uuid=project_uuid, - contents=contents, - modified=func.now(), - ) - .where(projects_comments.c.comment_id == comment_id) - .returning(literal_column("*")) - ) - result = await project_comment_result.first() - return ProjectsCommentsDB.model_validate(result) - - -async def delete_project_comment(conn, comment_id: CommentID) -> None: - await conn.execute( - projects_comments.delete().where(projects_comments.c.comment_id == comment_id) - ) - - -async def get_project_comment(conn, comment_id: CommentID) -> ProjectsCommentsDB: - project_comment_result = await conn.execute( - projects_comments.select().where(projects_comments.c.comment_id == comment_id) - ) - result = await project_comment_result.first() - return ProjectsCommentsDB.model_validate(result) diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_service.py b/services/web/server/src/simcore_service_webserver/projects/_comments_service.py deleted file mode 100644 index 7999d1e591ca..000000000000 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_service.py +++ /dev/null @@ -1,96 +0,0 @@ -import logging - -from aiohttp import web -from models_library.projects import ProjectID -from models_library.projects_comments import ( - CommentID, - ProjectsCommentsAPI, - ProjectsCommentsDB, -) -from models_library.users import UserID -from pydantic import PositiveInt - -from ._projects_repository_legacy import APP_PROJECT_DBAPI, ProjectDBAPI - -log = logging.getLogger(__name__) - - -# -# PROJECT COMMENTS ------------------------------------------------------------------- -# - - -async def create_project_comment( - request: web.Request, project_uuid: ProjectID, user_id: UserID, contents: str -) -> CommentID: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - comment_id: CommentID = await db.create_project_comment( - project_uuid, user_id, contents - ) - return comment_id - - -async def list_project_comments( - request: web.Request, - project_uuid: ProjectID, - offset: PositiveInt, - limit: int, -) -> list[ProjectsCommentsAPI]: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - projects_comments_db_model: list[ProjectsCommentsDB] = ( - await db.list_project_comments(project_uuid, offset, limit) - ) - projects_comments_api_model = [ - ProjectsCommentsAPI(**comment.model_dump()) - for comment in projects_comments_db_model - ] - return projects_comments_api_model - - -async def total_project_comments( - request: web.Request, - project_uuid: ProjectID, -) -> PositiveInt: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - project_comments_total: PositiveInt = await db.total_project_comments(project_uuid) - return project_comments_total - - -async def update_project_comment( - request: web.Request, - comment_id: CommentID, - project_uuid: ProjectID, - contents: str, -) -> ProjectsCommentsAPI: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - projects_comments_db_model: ProjectsCommentsDB = await db.update_project_comment( - comment_id, project_uuid, contents - ) - projects_comments_api_model = ProjectsCommentsAPI( - **projects_comments_db_model.model_dump() - ) - return projects_comments_api_model - - -async def delete_project_comment(request: web.Request, comment_id: CommentID) -> None: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - await db.delete_project_comment(comment_id) - - -async def get_project_comment( - request: web.Request, comment_id: CommentID -) -> ProjectsCommentsAPI: - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] - - projects_comments_db_model: ProjectsCommentsDB = await db.get_project_comment( - comment_id - ) - projects_comments_api_model = ProjectsCommentsAPI( - **projects_comments_db_model.model_dump() - ) - return projects_comments_api_model diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/comments_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/comments_rest.py deleted file mode 100644 index b8e1b3b746a5..000000000000 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/comments_rest.py +++ /dev/null @@ -1,228 +0,0 @@ -import logging -from typing import Any - -from aiohttp import web -from models_library.projects import ProjectID -from models_library.projects_comments import CommentID -from models_library.rest_pagination import ( - DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, - MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE, - Page, -) -from models_library.rest_pagination_utils import paginate_data -from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt -from servicelib.aiohttp import status -from servicelib.aiohttp.requests_validation import ( - parse_request_body_as, - parse_request_path_parameters_as, - parse_request_query_parameters_as, -) -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.rest_constants import RESPONSE_MODEL_POLICY - -from ..._meta import API_VTAG as VTAG -from ...login.decorators import login_required -from ...security.decorators import permission_required -from ...utils_aiohttp import envelope_json_response -from .. import _comments_service, _projects_service -from ._rest_exceptions import handle_plugin_requests_exceptions -from ._rest_schemas import AuthenticatedRequestContext - -_logger = logging.getLogger(__name__) - -# -# projects/*/comments COLLECTION ------------------------- -# - -routes = web.RouteTableDef() - - -class _ProjectCommentsPathParams(BaseModel): - project_uuid: ProjectID - model_config = ConfigDict(extra="forbid") - - -class _ProjectCommentsWithCommentPathParams(BaseModel): - project_uuid: ProjectID - comment_id: CommentID - model_config = ConfigDict(extra="forbid") - - -class _ProjectCommentsBodyParams(BaseModel): - contents: str - model_config = ConfigDict(extra="forbid") - - -@routes.post( - f"/{VTAG}/projects/{{project_uuid}}/comments", name="create_project_comment" -) -@login_required -@permission_required("project.read") -@handle_plugin_requests_exceptions -async def create_project_comment(request: web.Request): - req_ctx = AuthenticatedRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) - body_params = await parse_request_body_as(_ProjectCommentsBodyParams, request) - - # ensure the project exists - await _projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_uuid}", - user_id=req_ctx.user_id, - include_state=False, - ) - - comment_id = await _comments_service.create_project_comment( - request=request, - project_uuid=path_params.project_uuid, - user_id=req_ctx.user_id, - contents=body_params.contents, - ) - - return envelope_json_response({"comment_id": comment_id}, web.HTTPCreated) - - -class _ListProjectCommentsQueryParams(BaseModel): - limit: int = Field( - default=DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, - description="maximum number of items to return (pagination)", - ge=1, - lt=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE, - ) - offset: NonNegativeInt = Field( - default=0, description="index to the first item to return (pagination)" - ) - model_config = ConfigDict(extra="forbid") - - -@routes.get(f"/{VTAG}/projects/{{project_uuid}}/comments", name="list_project_comments") -@login_required -@permission_required("project.read") -@handle_plugin_requests_exceptions -async def list_project_comments(request: web.Request): - req_ctx = AuthenticatedRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) - query_params: _ListProjectCommentsQueryParams = parse_request_query_parameters_as( - _ListProjectCommentsQueryParams, request - ) - - # ensure the project exists - await _projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_uuid}", - user_id=req_ctx.user_id, - include_state=False, - ) - - total_project_comments = await _comments_service.total_project_comments( - request=request, - project_uuid=path_params.project_uuid, - ) - - project_comments = await _comments_service.list_project_comments( - request=request, - project_uuid=path_params.project_uuid, - offset=query_params.offset, - limit=query_params.limit, - ) - - page = Page[dict[str, Any]].model_validate( - paginate_data( - chunk=project_comments, - request_url=request.url, - total=total_project_comments, - limit=query_params.limit, - offset=query_params.offset, - ) - ) - return web.Response( - text=page.model_dump_json(**RESPONSE_MODEL_POLICY), - content_type=MIMETYPE_APPLICATION_JSON, - ) - - -@routes.put( - f"/{VTAG}/projects/{{project_uuid}}/comments/{{comment_id}}", - name="update_project_comment", -) -@login_required -@permission_required("project.read") -@handle_plugin_requests_exceptions -async def update_project_comment(request: web.Request): - req_ctx = AuthenticatedRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as( - _ProjectCommentsWithCommentPathParams, request - ) - body_params = await parse_request_body_as(_ProjectCommentsBodyParams, request) - - # ensure the project exists - await _projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_uuid}", - user_id=req_ctx.user_id, - include_state=False, - ) - - updated_comment = await _comments_service.update_project_comment( - request=request, - comment_id=path_params.comment_id, - project_uuid=path_params.project_uuid, - contents=body_params.contents, - ) - return envelope_json_response(updated_comment) - - -@routes.delete( - f"/{VTAG}/projects/{{project_uuid}}/comments/{{comment_id}}", - name="delete_project_comment", -) -@login_required -@permission_required("project.read") -@handle_plugin_requests_exceptions -async def delete_project_comment(request: web.Request): - req_ctx = AuthenticatedRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as( - _ProjectCommentsWithCommentPathParams, request - ) - - # ensure the project exists - await _projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_uuid}", - user_id=req_ctx.user_id, - include_state=False, - ) - - await _comments_service.delete_project_comment( - request=request, - comment_id=path_params.comment_id, - ) - return web.json_response(status=status.HTTP_204_NO_CONTENT) - - -@routes.get( - f"/{VTAG}/projects/{{project_uuid}}/comments/{{comment_id}}", - name="get_project_comment", -) -@login_required -@permission_required("project.read") -@handle_plugin_requests_exceptions -async def get_project_comment(request: web.Request): - req_ctx = AuthenticatedRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as( - _ProjectCommentsWithCommentPathParams, request - ) - - # ensure the project exists - await _projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_uuid}", - user_id=req_ctx.user_id, - include_state=False, - ) - - comment = await _comments_service.get_project_comment( - request=request, - comment_id=path_params.comment_id, - ) - return envelope_json_response(comment) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_repository_legacy.py b/services/web/server/src/simcore_service_webserver/projects/_projects_repository_legacy.py index 32bd7c00a539..a677754e5235 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_repository_legacy.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_repository_legacy.py @@ -25,7 +25,6 @@ ProjectListAtDB, ProjectTemplateType, ) -from models_library.projects_comments import CommentID, ProjectsCommentsDB from models_library.projects_nodes import Node from models_library.projects_nodes_io import NodeID, NodeIDStr from models_library.resource_tracker import ( @@ -78,14 +77,6 @@ from ..application_settings import get_application_settings from ..models import ClientSessionID from ..utils import now_str -from ._comments_repository import ( - create_project_comment, - delete_project_comment, - get_project_comment, - list_project_comments, - total_project_comments, - update_project_comment, -) from ._project_document_service import create_project_document_and_increment_version from ._projects_repository import PROJECT_DB_COLS from ._projects_repository_legacy_utils import ( @@ -1244,51 +1235,6 @@ async def get_tags_by_project(self, project_id: str) -> list[int]: ) return [row.tag_id async for row in conn.execute(query)] - # - # Project Comments - # - - async def create_project_comment( - self, project_uuid: ProjectID, user_id: UserID, contents: str - ) -> CommentID: - async with self.engine.acquire() as conn: - return await create_project_comment(conn, project_uuid, user_id, contents) - - async def list_project_comments( - self, - project_uuid: ProjectID, - offset: PositiveInt, - limit: int, - ) -> list[ProjectsCommentsDB]: - async with self.engine.acquire() as conn: - return await list_project_comments(conn, project_uuid, offset, limit) - - async def total_project_comments( - self, - project_uuid: ProjectID, - ) -> PositiveInt: - async with self.engine.acquire() as conn: - return await total_project_comments(conn, project_uuid) - - async def update_project_comment( - self, - comment_id: CommentID, - project_uuid: ProjectID, - contents: str, - ) -> ProjectsCommentsDB: - async with self.engine.acquire() as conn: - return await update_project_comment( - conn, comment_id, project_uuid, contents - ) - - async def delete_project_comment(self, comment_id: CommentID) -> None: - async with self.engine.acquire() as conn: - return await delete_project_comment(conn, comment_id) - - async def get_project_comment(self, comment_id: CommentID) -> ProjectsCommentsDB: - async with self.engine.acquire() as conn: - return await get_project_comment(conn, comment_id) - # # Project Wallet # diff --git a/services/web/server/src/simcore_service_webserver/projects/plugin.py b/services/web/server/src/simcore_service_webserver/projects/plugin.py index e714ed350d73..e46d40744431 100644 --- a/services/web/server/src/simcore_service_webserver/projects/plugin.py +++ b/services/web/server/src/simcore_service_webserver/projects/plugin.py @@ -13,7 +13,6 @@ from ..rabbitmq import setup_rabbitmq from ._controller import ( access_rights_rest, - comments_rest, conversations_rest, folders_rest, metadata_rest, @@ -69,7 +68,6 @@ def setup_projects(app: web.Application) -> bool: # setup REST-controllers app.router.add_routes(projects_states_rest.routes) app.router.add_routes(projects_rest.routes) - app.router.add_routes(comments_rest.routes) app.router.add_routes(conversations_rest.routes) app.router.add_routes(access_rights_rest.routes) app.router.add_routes(metadata_rest.routes) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py deleted file mode 100644 index 9a187a1d0816..000000000000 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py +++ /dev/null @@ -1,228 +0,0 @@ -# pylint: disable=protected-access -# pylint: disable=redefined-outer-name -# pylint: disable=too-many-arguments -# pylint: disable=unused-argument -# pylint: disable=unused-variable -# pylint: disable=too-many-statements - - -from http import HTTPStatus - -import pytest -import sqlalchemy as sa -from aiohttp.test_utils import TestClient -from pytest_simcore.helpers.assert_checks import assert_status -from pytest_simcore.helpers.webserver_login import LoggedUser, UserInfoDict -from servicelib.aiohttp import status -from simcore_service_webserver._meta import api_version_prefix -from simcore_service_webserver.db.models import UserRole -from simcore_service_webserver.projects._groups_repository import ( - update_or_insert_project_group, -) -from simcore_service_webserver.projects.models import ProjectDict - -API_PREFIX = "/" + api_version_prefix - - -@pytest.mark.parametrize( - "user_role,expected", - [ - (UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED), - (UserRole.GUEST, status.HTTP_200_OK), - (UserRole.USER, status.HTTP_200_OK), - (UserRole.TESTER, status.HTTP_200_OK), - ], -) -async def test_project_comments_user_role_access( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - user_role: UserRole, - expected: HTTPStatus, -): - assert client.app - base_url = client.app.router["list_project_comments"].url_for( - project_uuid=user_project["uuid"] - ) - resp = await client.get(f"{base_url}") - assert resp.status == 401 if user_role == UserRole.ANONYMOUS else 200 - - -@pytest.mark.acceptance_test( - "https://github.com/ITISFoundation/osparc-issues/issues/993" -) -@pytest.mark.parametrize( - "user_role,expected", - [ - (UserRole.USER, status.HTTP_200_OK), - ], -) -async def test_project_comments_full_workflow( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - expected: HTTPStatus, - postgres_db: sa.engine.Engine, -): - base_url = client.app.router["list_project_comments"].url_for( - project_uuid=user_project["uuid"] - ) - resp = await client.get(f"{base_url}") - data, _, meta, links = await assert_status( - resp, - expected, - include_meta=True, - include_links=True, - ) - assert data == [] - assert meta["total"] == 0 - assert links - - # Now we will add first comment - body = {"contents": "My first comment"} - resp = await client.post(f"{base_url}", json=body) - data, _ = await assert_status( - resp, - status.HTTP_201_CREATED, - ) - first_comment_id = data["comment_id"] - - # Now we will add second comment - resp = await client.post(f"{base_url}", json={"contents": "My second comment"}) - data, _ = await assert_status( - resp, - status.HTTP_201_CREATED, - ) - second_comment_id = data["comment_id"] - - # Now we will list all comments for the project - resp = await client.get(f"{base_url}") - data, _, meta, links = await assert_status( - resp, - expected, - include_meta=True, - include_links=True, - ) - assert len(data) == 2 - assert meta["total"] == 2 - assert links - - # Now we will update the second comment - updated_comment = "Updated second comment" - resp = await client.put( - f"{base_url}/{second_comment_id}", - json={"contents": updated_comment}, - ) - data, _ = await assert_status( - resp, - expected, - ) - - # Now we will get the second comment - resp = await client.get(f"{base_url}/{second_comment_id}") - data, _ = await assert_status( - resp, - expected, - ) - assert data["contents"] == updated_comment - - # Now we will delete the second comment - resp = await client.delete(f"{base_url}/{second_comment_id}") - data, _ = await assert_status( - resp, - status.HTTP_204_NO_CONTENT, - ) - - # Now we will list all comments for the project - resp = await client.get(f"{base_url}") - data, _, meta, links = await assert_status( - resp, - expected, - include_meta=True, - include_links=True, - ) - assert meta["total"] == 1 - assert links - assert len(data) == 1 - - # Now we will log as a different user - async with LoggedUser(client) as new_logged_user: - # As this user does not have access to the project, they should get 403 - resp = await client.get(f"{base_url}") - _, errors = await assert_status( - resp, - status.HTTP_403_FORBIDDEN, - ) - assert errors - - resp = await client.get(f"{base_url}/{first_comment_id}") - _, errors = await assert_status( - resp, - status.HTTP_403_FORBIDDEN, - ) - assert errors - - # Now we will share the project with the new user - await update_or_insert_project_group( - client.app, - project_id=user_project["uuid"], - group_id=new_logged_user["primary_gid"], - read=True, - write=True, - delete=True, - ) - - # Now the user should have access to the project now - # New user will add comment - resp = await client.post( - f"{base_url}", - json={"contents": "My first comment as a new user"}, - ) - data, _ = await assert_status( - resp, - status.HTTP_201_CREATED, - ) - new_user_comment_id = data["comment_id"] - - # New user will modify the comment - updated_comment = "Updated My first comment as a new user" - resp = await client.put( - f"{base_url}/{new_user_comment_id}", - json={"contents": updated_comment}, - ) - data, _ = await assert_status( - resp, - expected, - ) - assert data["contents"] == updated_comment - - # New user will list all comments - resp = await client.get(f"{base_url}") - data, _, meta, links = await assert_status( - resp, - expected, - include_meta=True, - include_links=True, - ) - assert meta["total"] == 2 - assert links - assert len(data) == 2 - - # New user will modify comment of the previous user - updated_comment = "Updated comment of previous user" - resp = await client.put( - f"{base_url}/{first_comment_id}", - json={"contents": updated_comment}, - ) - data, _ = await assert_status( - resp, - expected, - ) - assert data["contents"] == updated_comment - - # New user will delete comment of the previous user - resp = await client.delete(f"{base_url}/{first_comment_id}") - data, _ = await assert_status( - resp, - status.HTTP_204_NO_CONTENT, - )