Skip to content

Commit 9e99973

Browse files
authored
♻️ Refactor and Upgrade Users Repository including users_secrets split (#8124)
1 parent f1c60a8 commit 9e99973

File tree

56 files changed

+1399
-690
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1399
-690
lines changed

packages/notifications-library/tests/with_db/conftest.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
from models_library.users import UserID
1717
from notifications_library._templates import get_default_named_templates
1818
from pydantic import validate_call
19+
from pytest_simcore.helpers.postgres_tools import insert_and_get_row_lifespan
20+
from pytest_simcore.helpers.postgres_users import (
21+
insert_and_get_user_and_secrets_lifespan,
22+
)
1923
from simcore_postgres_database.models.jinja2_templates import jinja2_templates
2024
from simcore_postgres_database.models.payments_transactions import payments_transactions
2125
from simcore_postgres_database.models.products import products
2226
from simcore_postgres_database.models.products_to_templates import products_to_templates
23-
from simcore_postgres_database.models.users import users
2427
from sqlalchemy.engine.row import Row
2528
from sqlalchemy.ext.asyncio.engine import AsyncEngine
2629

@@ -50,16 +53,11 @@ async def user(
5053
and injects a user in db
5154
"""
5255
assert user_id == user["id"]
53-
pk_args = users.c.id, user["id"]
54-
55-
# NOTE: creation of primary group and setting `groupid`` is automatically triggered after creation of user by postgres
56-
async with sqlalchemy_async_engine.begin() as conn:
57-
row: Row = await _insert_and_get_row(conn, users, user, *pk_args)
58-
59-
yield row._asdict()
60-
61-
async with sqlalchemy_async_engine.begin() as conn:
62-
await _delete_row(conn, users, *pk_args)
56+
async with insert_and_get_user_and_secrets_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup
57+
sqlalchemy_async_engine,
58+
**user,
59+
) as row:
60+
yield row
6361

6462

6563
@pytest.fixture
@@ -82,15 +80,14 @@ async def product(
8280
# NOTE: osparc product is already in db. This is another product
8381
assert product["name"] != "osparc"
8482

85-
pk_args = products.c.name, product["name"]
86-
87-
async with sqlalchemy_async_engine.begin() as conn:
88-
row: Row = await _insert_and_get_row(conn, products, product, *pk_args)
89-
90-
yield row._asdict()
91-
92-
async with sqlalchemy_async_engine.begin() as conn:
93-
await _delete_row(conn, products, *pk_args)
83+
async with insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup
84+
sqlalchemy_async_engine,
85+
table=products,
86+
values=product,
87+
pk_col=products.c.name,
88+
pk_value=product["name"],
89+
) as row:
90+
yield row
9491

9592

9693
@pytest.fixture
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""new users secrets
2+
3+
Revision ID: 5679165336c8
4+
Revises: 61b98a60e934
5+
Create Date: 2025-07-17 17:07:20.200038+00:00
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "5679165336c8"
14+
down_revision = "61b98a60e934"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table(
21+
"users_secrets",
22+
sa.Column("user_id", sa.BigInteger(), nullable=False),
23+
sa.Column("password_hash", sa.String(), nullable=False),
24+
sa.Column(
25+
"modified",
26+
sa.DateTime(timezone=True),
27+
server_default=sa.text("now()"),
28+
nullable=False,
29+
),
30+
sa.ForeignKeyConstraint(
31+
["user_id"],
32+
["users.id"],
33+
name="fk_users_secrets_user_id_users",
34+
onupdate="CASCADE",
35+
ondelete="CASCADE",
36+
),
37+
sa.PrimaryKeyConstraint("user_id", name="users_secrets_pkey"),
38+
)
39+
40+
# Copy password data from users table to users_secrets table
41+
op.execute(
42+
sa.DDL(
43+
"""
44+
INSERT INTO users_secrets (user_id, password_hash, modified)
45+
SELECT id, password_hash, created_at
46+
FROM users
47+
WHERE password_hash IS NOT NULL
48+
"""
49+
)
50+
)
51+
52+
op.drop_column("users", "password_hash")
53+
54+
55+
def downgrade():
56+
# Add column as nullable first
57+
op.add_column(
58+
"users",
59+
sa.Column("password_hash", sa.VARCHAR(), autoincrement=False, nullable=True),
60+
)
61+
62+
# Copy password data back from users_secrets table to users table
63+
op.execute(
64+
sa.DDL(
65+
"""
66+
UPDATE users
67+
SET password_hash = us.password_hash
68+
FROM users_secrets us
69+
WHERE users.id = us.user_id
70+
"""
71+
)
72+
)
73+
74+
# Now make the column NOT NULL
75+
op.alter_column("users", "password_hash", nullable=False)
76+
77+
op.drop_table("users_secrets")

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,28 @@ class RefActions:
1616
NO_ACTION: Final[str] = "NO ACTION"
1717

1818

19-
def column_created_datetime(*, timezone: bool = True) -> sa.Column:
19+
def column_created_datetime(
20+
*, timezone: bool = True, doc="Timestamp auto-generated upon creation"
21+
) -> sa.Column:
2022
return sa.Column(
2123
"created",
2224
sa.DateTime(timezone=timezone),
2325
nullable=False,
2426
server_default=sa.sql.func.now(),
25-
doc="Timestamp auto-generated upon creation",
27+
doc=doc,
2628
)
2729

2830

29-
def column_modified_datetime(*, timezone: bool = True) -> sa.Column:
31+
def column_modified_datetime(
32+
*, timezone: bool = True, doc="Timestamp with last row update"
33+
) -> sa.Column:
3034
return sa.Column(
3135
"modified",
3236
sa.DateTime(timezone=timezone),
3337
nullable=False,
3438
server_default=sa.sql.func.now(),
3539
onupdate=sa.sql.func.now(),
36-
doc="Timestamp with last row update",
40+
doc=doc,
3741
)
3842

3943

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,6 @@
6767
"NOTE: new policy (NK) is that the same phone can be reused therefore it does not has to be unique",
6868
),
6969
#
70-
# User Secrets ------------------
71-
#
72-
sa.Column(
73-
"password_hash",
74-
sa.String(),
75-
nullable=False,
76-
doc="Hashed password",
77-
),
78-
#
7970
# User Account ------------------
8071
#
8172
sa.Column(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import sqlalchemy as sa
2+
3+
from ._common import RefActions, column_modified_datetime
4+
from .base import metadata
5+
6+
__all__: tuple[str, ...] = ("users_secrets",)
7+
8+
users_secrets = sa.Table(
9+
"users_secrets",
10+
metadata,
11+
#
12+
# User Secrets ------------------
13+
#
14+
sa.Column(
15+
"user_id",
16+
sa.BigInteger(),
17+
sa.ForeignKey(
18+
"users.id",
19+
name="fk_users_secrets_user_id_users",
20+
onupdate=RefActions.CASCADE,
21+
ondelete=RefActions.CASCADE,
22+
),
23+
nullable=False,
24+
),
25+
sa.Column(
26+
"password_hash",
27+
sa.String(),
28+
nullable=False,
29+
doc="Hashed password",
30+
),
31+
column_modified_datetime(timezone=True, doc="Last password modification timestamp"),
32+
# ---------------------------
33+
sa.PrimaryKeyConstraint("user_id", name="users_secrets_pkey"),
34+
)

0 commit comments

Comments
 (0)