Skip to content

Commit 4c2ae8f

Browse files
committed
[owl] Notification Schema Updates (#916)
* Update schema * DB migration
1 parent 1c946e0 commit 4c2ae8f

9 files changed

Lines changed: 317 additions & 210 deletions

File tree

clients/python/src/jamaibase/client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4108,12 +4108,14 @@ async def delete_notification(
41084108

41094109
async def set_opened(
41104110
self,
4111-
notification_group_id: str,
4111+
notification_group_ids: str | list[str],
41124112
**kwargs,
41134113
) -> OkResponse:
4114+
if isinstance(notification_group_ids, str):
4115+
notification_group_ids = [notification_group_ids]
41144116
return await self._patch(
41154117
"/v2/notifications/opened",
4116-
params=dict(notification_group_id=notification_group_id),
4118+
params=dict(notification_group_ids=notification_group_ids),
41174119
response_model=OkResponse,
41184120
**kwargs,
41194121
)
@@ -6817,8 +6819,8 @@ def get_notification(self, notification_group_id: str, **kwargs) -> Notification
68176819
def delete_notification(self, notification_group_id: str, **kwargs) -> OkResponse:
68186820
return LOOP.run(super().delete_notification(notification_group_id, **kwargs))
68196821

6820-
def set_opened(self, notification_group_id: str, **kwargs) -> OkResponse:
6821-
return LOOP.run(super().set_opened(notification_group_id, **kwargs))
6822+
def set_opened(self, notification_group_ids: str | list[str], **kwargs) -> OkResponse:
6823+
return LOOP.run(super().set_opened(notification_group_ids, **kwargs))
68226824

68236825
def set_all_opened(self, **kwargs) -> OkResponse:
68246826
return LOOP.run(super().set_all_opened(**kwargs))

clients/python/src/jamaibase/types/db.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,10 +1374,18 @@ class NotificationGroupCreate(_BaseModel):
13741374
None,
13751375
description="ID of the user who triggered the event.",
13761376
)
1377+
subject_id: str | None = Field(
1378+
None,
1379+
description="ID of the subject user (e.g. invitee, new owner).",
1380+
)
13771381
recipient_ids: list[str] = Field(
13781382
[],
13791383
description="Explicit recipient user IDs. Used for fan-out.",
13801384
)
1385+
message: str = Field(
1386+
"",
1387+
description="Notification text (Markdown).",
1388+
)
13811389

13821390

13831391
class NotificationGroup_(NotificationGroupCreate, _TableBase):
@@ -1391,6 +1399,10 @@ class NotificationGroupRead(NotificationGroup_):
13911399
None,
13921400
description="User who triggered the event.",
13931401
)
1402+
subject: "User_ | None" = Field(
1403+
None,
1404+
description="Subject user (e.g. invitee, new owner).",
1405+
)
13941406

13951407

13961408
class NotificationCreate(_BaseModel):
@@ -1400,8 +1412,8 @@ class NotificationCreate(_BaseModel):
14001412
notification_group_id: str = Field(
14011413
description="Notification group ID.",
14021414
)
1403-
body: str = Field(
1404-
description="Notification body text.",
1415+
message: str = Field(
1416+
description="Notification text (Markdown).",
14051417
)
14061418

14071419

services/api/src/owl/db/__init__.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,56 @@ async def _migrate_modeltype_enum(engine: AsyncEngine) -> bool:
649649
return True
650650

651651

652+
async def _migrate_notification_schema(engine: AsyncEngine) -> bool:
653+
"""
654+
Migrate NotificationGroup and Notification tables:
655+
- Add `subject_id` (FK→User SET NULL) and `message` columns to NotificationGroup
656+
- Rename `body` → `message` in Notification
657+
"""
658+
group_table = "NotificationGroup"
659+
notif_table = "Notification"
660+
661+
async with engine.connect() as conn:
662+
group_has_message = await _check_column_exists(conn, group_table, "message")
663+
group_has_subject = await _check_column_exists(conn, group_table, "subject_id")
664+
notif_has_message = await _check_column_exists(conn, notif_table, "message")
665+
666+
if group_has_message and group_has_subject and notif_has_message:
667+
return False
668+
669+
async with engine.begin() as conn:
670+
if not group_has_subject:
671+
await conn.execute(
672+
text(
673+
f"""
674+
ALTER TABLE {SCHEMA}."{group_table}"
675+
ADD COLUMN subject_id TEXT DEFAULT NULL
676+
REFERENCES {SCHEMA}."User"(id) ON DELETE SET NULL;
677+
"""
678+
)
679+
)
680+
if not group_has_message:
681+
await conn.execute(
682+
text(
683+
f"""
684+
ALTER TABLE {SCHEMA}."{group_table}"
685+
ADD COLUMN message TEXT NOT NULL DEFAULT '';
686+
"""
687+
)
688+
)
689+
if not notif_has_message:
690+
await conn.execute(
691+
text(
692+
f"""
693+
ALTER TABLE {SCHEMA}."{notif_table}"
694+
RENAME COLUMN body TO message;
695+
"""
696+
)
697+
)
698+
logger.info("Successfully migrated notification schema (subject_id, message, body→message).")
699+
return True
700+
701+
652702
async def migrate_db():
653703
engine = create_db_engine_async()
654704
migrated = [
@@ -664,6 +714,7 @@ async def migrate_db():
664714
await _backfill_price_plan_image_products(engine),
665715
await _migrate_verification_codes(engine),
666716
await _migrate_reasoning_jsonb_keys(engine),
717+
await _migrate_notification_schema(engine),
667718
]
668719
if any(migrated):
669720
logger.success("DB migrations performed.")

services/api/src/owl/db/models/oss.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,29 @@ class NotificationGroup(_TableBase, table=True):
16271627
ondelete="SET NULL",
16281628
description="ID of the user who triggered the event.",
16291629
)
1630-
actor: "User" = _relationship(None, selectin=True, cascade=None)
1630+
subject_id: str | None = SqlField(
1631+
None,
1632+
foreign_key="User.id",
1633+
nullable=True,
1634+
ondelete="SET NULL",
1635+
description="ID of the subject user (e.g. invitee, new owner).",
1636+
)
1637+
message: str = SqlField(
1638+
"",
1639+
description="Notification message text (Markdown).",
1640+
)
1641+
actor: "User" = _relationship(
1642+
None,
1643+
selectin=True,
1644+
cascade=None,
1645+
sa_kwargs=dict(foreign_keys="[NotificationGroup.actor_id]"),
1646+
)
1647+
subject: "User" = _relationship(
1648+
None,
1649+
selectin=True,
1650+
cascade=None,
1651+
sa_kwargs=dict(foreign_keys="[NotificationGroup.subject_id]"),
1652+
)
16311653
notifications: list["Notification"] = _relationship("notification_group")
16321654

16331655

@@ -1644,8 +1666,9 @@ class Notification(_TableBase, table=True):
16441666
ondelete="CASCADE",
16451667
description="Notification group ID.",
16461668
)
1647-
body: str = SqlField(
1648-
description="Notification body text.",
1669+
message: str = SqlField(
1670+
"",
1671+
description="Notification message text (Markdown).",
16491672
)
16501673
opened_at: DatetimeUTC | None = SqlField(
16511674
None,

services/api/src/owl/routers/notification.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ async def create_notification_group(
5555
intent = NotificationIntent(
5656
scope=body.scope,
5757
event_type=body.event_type,
58-
meta=body.meta,
58+
message=body.message,
5959
actor_id=body.actor_id,
60+
subject_id=body.subject_id,
6061
organization_id=body.organization_id,
6162
project_id=body.project_id,
6263
recipient_ids=body.recipient_ids,
@@ -209,23 +210,28 @@ async def delete_notification(
209210

210211
@router.patch(
211212
"/v2/notifications/opened",
212-
summary="Mark a notification as opened.",
213+
summary="Mark one or more notifications as opened.",
213214
description="Permissions: notification owner.",
214215
)
215216
@handle_exception
216217
async def set_opened(
217218
user: Annotated[UserAuth, Depends(auth_user_service_key)],
218219
session: Annotated[AsyncSession, Depends(yield_async_session)],
219-
notification_group_id: Annotated[
220-
str, Query(min_length=1, description="Notification group ID.")
220+
notification_group_ids: Annotated[
221+
list[str], Query(min_length=1, description="List of notification group IDs.")
221222
],
222223
) -> OkResponse:
223-
notif = await session.get(Notification, (user.id, notification_group_id))
224-
if notif is None or notif.deleted_at is not None:
225-
raise ResourceNotFoundError("Notification not found.")
226224
timestamp = now()
227-
notif.opened_at = timestamp
228-
notif.updated_at = timestamp
225+
await session.exec(
226+
update(Notification)
227+
.where(
228+
Notification.user_id == user.id,
229+
Notification.notification_group_id.in_(notification_group_ids),
230+
Notification.opened_at.is_(None),
231+
Notification.deleted_at.is_(None),
232+
)
233+
.values(opened_at=timestamp, updated_at=timestamp)
234+
)
229235
await session.commit()
230236
return OkResponse()
231237

0 commit comments

Comments
 (0)