core-framework 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core_framework/__init__.py +0 -0
- core_framework/alembic/comment/alembic/README +1 -0
- core_framework/alembic/comment/alembic/env.py +72 -0
- core_framework/alembic/comment/alembic/script.py.mako +28 -0
- core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
- core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
- core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
- core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
- core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
- core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
- core_framework/alembic/comment/alembic.ini +52 -0
- core_framework/alembic/extension/alembic/README +1 -0
- core_framework/alembic/extension/alembic/env.py +98 -0
- core_framework/alembic/extension/alembic/script.py.mako +28 -0
- core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
- core_framework/alembic/extension/alembic.ini +147 -0
- core_framework/alembic/moderation/alembic/README +1 -0
- core_framework/alembic/moderation/alembic/env.py +98 -0
- core_framework/alembic/moderation/alembic/script.py.mako +28 -0
- core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
- core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
- core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
- core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
- core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
- core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
- core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
- core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
- core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
- core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
- core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
- core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
- core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
- core_framework/alembic/moderation/alembic.ini +147 -0
- core_framework/alembic/post/alembic/README +1 -0
- core_framework/alembic/post/alembic/env.py +97 -0
- core_framework/alembic/post/alembic/script.py.mako +28 -0
- core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
- core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
- core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
- core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
- core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
- core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
- core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
- core_framework/alembic/post/alembic.ini +149 -0
- core_framework/alembic/user/alembic/README +1 -0
- core_framework/alembic/user/alembic/env.py +98 -0
- core_framework/alembic/user/alembic/script.py.mako +28 -0
- core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
- core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
- core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
- core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
- core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
- core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
- core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
- core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
- core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
- core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
- core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
- core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
- core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
- core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
- core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
- core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
- core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
- core_framework/alembic/user/alembic.ini +147 -0
- core_framework/api/__init__.py +0 -0
- core_framework/api/admin/__init__.py +0 -0
- core_framework/api/admin/comments/router.py +69 -0
- core_framework/api/admin/comments/schemas.py +53 -0
- core_framework/api/admin/moderation/__init__.py +0 -0
- core_framework/api/admin/moderation/router.py +205 -0
- core_framework/api/admin/moderation/schemas.py +110 -0
- core_framework/api/admin/posts/router.py +62 -0
- core_framework/api/admin/posts/schemas.py +29 -0
- core_framework/api/admin/router.py +17 -0
- core_framework/api/admin/users/__init__.py +0 -0
- core_framework/api/admin/users/router.py +181 -0
- core_framework/api/admin/users/schemas.py +137 -0
- core_framework/api/auth/__init__.py +0 -0
- core_framework/api/auth/router.py +21 -0
- core_framework/api/auth/schemas.py +28 -0
- core_framework/api/comments/authenticated/router.py +126 -0
- core_framework/api/comments/authenticated/schemas.py +27 -0
- core_framework/api/comments/public/router.py +103 -0
- core_framework/api/comments/public/schemas.py +36 -0
- core_framework/api/comments/router.py +9 -0
- core_framework/api/comments/schemas.py +17 -0
- core_framework/api/dependencies.py +168 -0
- core_framework/api/events/router.py +39 -0
- core_framework/api/events/schemas.py +20 -0
- core_framework/api/posts/authenticated/router.py +83 -0
- core_framework/api/posts/authenticated/schemas.py +37 -0
- core_framework/api/posts/public/router.py +100 -0
- core_framework/api/posts/public/schemas.py +39 -0
- core_framework/api/posts/router.py +9 -0
- core_framework/api/posts/schemas.py +39 -0
- core_framework/api/router.py +19 -0
- core_framework/api/schemas.py +9 -0
- core_framework/api/system/__init__.py +0 -0
- core_framework/api/system/router.py +108 -0
- core_framework/api/users/__init__.py +0 -0
- core_framework/api/users/authenticated/__init__.py +0 -0
- core_framework/api/users/authenticated/router.py +244 -0
- core_framework/api/users/authenticated/schemas.py +81 -0
- core_framework/api/users/public/__init__.py +0 -0
- core_framework/api/users/public/router.py +25 -0
- core_framework/api/users/public/schemas.py +7 -0
- core_framework/api/users/router.py +9 -0
- core_framework/api/users/shared/schemas.py +174 -0
- core_framework/application/__init__.py +0 -0
- core_framework/application/auth/__init__.py +0 -0
- core_framework/application/auth/access_service.py +26 -0
- core_framework/application/auth/auth_service.py +10 -0
- core_framework/application/auth/models.py +10 -0
- core_framework/application/bootstrap.py +19 -0
- core_framework/application/comments/admin_service.py +236 -0
- core_framework/application/comments/aggregation_service.py +28 -0
- core_framework/application/comments/authenticated_service.py +89 -0
- core_framework/application/comments/public_service.py +218 -0
- core_framework/application/events/README.md +26 -0
- core_framework/application/events/event_service.py +51 -0
- core_framework/application/events/event_token.py +46 -0
- core_framework/application/events/models.py +9 -0
- core_framework/application/moderation/__init__.py +0 -0
- core_framework/application/moderation/appeal_service.py +98 -0
- core_framework/application/moderation/moderator_service.py +46 -0
- core_framework/application/moderation/report_service.py +180 -0
- core_framework/application/moderation/scheduled_service.py +5 -0
- core_framework/application/moderation/user_service.py +180 -0
- core_framework/application/posts/admin_service.py +104 -0
- core_framework/application/posts/aggregation_service.py +28 -0
- core_framework/application/posts/authenticated_service.py +72 -0
- core_framework/application/posts/public_service.py +197 -0
- core_framework/application/shared/__init__.py +0 -0
- core_framework/application/shared/enums.py +16 -0
- core_framework/application/shared/exceptions.py +16 -0
- core_framework/application/shared/user_agent.py +24 -0
- core_framework/application/users/__init__.py +0 -0
- core_framework/application/users/admin_service.py +298 -0
- core_framework/application/users/authenticated_service.py +179 -0
- core_framework/application/users/public_service.py +7 -0
- core_framework/application/users/scheduled_service.py +5 -0
- core_framework/bundled_alembic.py +57 -0
- core_framework/core/__init__.py +37 -0
- core_framework/core/cache.py +234 -0
- core_framework/core/context.py +14 -0
- core_framework/core/database.py +111 -0
- core_framework/core/exception_handlers/__init__.py +3 -0
- core_framework/core/exception_handlers/comment.py +99 -0
- core_framework/core/exception_handlers/common.py +5 -0
- core_framework/core/exception_handlers/moderation.py +104 -0
- core_framework/core/exception_handlers/post.py +54 -0
- core_framework/core/exception_handlers/setup.py +80 -0
- core_framework/core/exception_handlers/user.py +72 -0
- core_framework/core/http_client.py +64 -0
- core_framework/core/logging.py +99 -0
- core_framework/core/middleware.py +64 -0
- core_framework/core/observability.py +36 -0
- core_framework/core/pagination.py +203 -0
- core_framework/core/redis.py +135 -0
- core_framework/core/runtime.py +66 -0
- core_framework/core/settings.py +189 -0
- core_framework/domains/__init__.py +0 -0
- core_framework/domains/comment/README.md +243 -0
- core_framework/domains/comment/__init__.py +25 -0
- core_framework/domains/comment/constants.py +3 -0
- core_framework/domains/comment/dependencies.py +29 -0
- core_framework/domains/comment/enums.py +11 -0
- core_framework/domains/comment/exceptions.py +31 -0
- core_framework/domains/comment/models.py +54 -0
- core_framework/domains/comment/repository.py +947 -0
- core_framework/domains/comment/service.py +259 -0
- core_framework/domains/moderation/README.md +138 -0
- core_framework/domains/moderation/__init__.py +47 -0
- core_framework/domains/moderation/dependencies.py +29 -0
- core_framework/domains/moderation/enums.py +62 -0
- core_framework/domains/moderation/exceptions.py +31 -0
- core_framework/domains/moderation/models.py +94 -0
- core_framework/domains/moderation/repository.py +828 -0
- core_framework/domains/moderation/service.py +334 -0
- core_framework/domains/post/README.md +182 -0
- core_framework/domains/post/__init__.py +22 -0
- core_framework/domains/post/constants.py +3 -0
- core_framework/domains/post/dependencies.py +29 -0
- core_framework/domains/post/enums.py +18 -0
- core_framework/domains/post/exceptions.py +21 -0
- core_framework/domains/post/models.py +53 -0
- core_framework/domains/post/repository.py +791 -0
- core_framework/domains/post/service.py +204 -0
- core_framework/domains/user/README.md +74 -0
- core_framework/domains/user/__init__.py +39 -0
- core_framework/domains/user/constants.py +8 -0
- core_framework/domains/user/dependencies.py +29 -0
- core_framework/domains/user/enums.py +19 -0
- core_framework/domains/user/exceptions.py +31 -0
- core_framework/domains/user/models.py +124 -0
- core_framework/domains/user/repository.py +612 -0
- core_framework/domains/user/service.py +257 -0
- core_framework/domains/user/utils.py +182 -0
- core_framework/main.py +104 -0
- core_framework/worker/__init__.py +0 -0
- core_framework/worker/main.py +56 -0
- core_framework/worker/schedules/__init__.py +35 -0
- core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
- core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
- core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
- core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
- core_framework/worker/tasks/__init__.py +11 -0
- core_framework/worker/tasks/process_account_deletion.py +13 -0
- core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
- core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
- core_framework/worker/tasks/process_mute_lift.py +13 -0
- core_framework-0.3.0.dist-info/METADATA +22 -0
- core_framework-0.3.0.dist-info/RECORD +222 -0
- core_framework-0.3.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Comment Domain
|
|
2
|
+
|
|
3
|
+
## Implementation Status
|
|
4
|
+
|
|
5
|
+
- **Implemented**: Root and reply creation; comment retrieval (by subject, replies under a parent, by author with viewer restrictions/blocks). Materialized path stored as PostgreSQL `ltree` (path labels are comment IDs); max reply depth enforced in application code. Author comment edit with a fixed max edit count and `edited_count` / `edited_at`. Soft delete via `comment_status` (`active` / `deleted`) for the author’s own comments and status updates by comment id (used by admin inactive/restore flows outside this package). Comment likes (idempotent like/unlike; counts feed into stats). Engagement stats materialized in `comment_stats`, driven by a `comment_stats_dirty` table and periodic aggregation (see `docs/flows/comments/comment_stats_aggregation.md`).
|
|
6
|
+
- **Planned**: Media attachments on comments (metadata, ordering, processing lifecycle as described in this document).
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
Owns comment content lifecycle, subject attachment, reply hierarchy, media attachments, and comment likes. Provides the authoritative source for comment existence and current content state. Comments are always attached to a subject (e.g., post, user wall); the subject cannot be null.
|
|
11
|
+
|
|
12
|
+
## Owns
|
|
13
|
+
|
|
14
|
+
### Comment Content
|
|
15
|
+
|
|
16
|
+
- Comment creation, retrieval, editing, and deletion state
|
|
17
|
+
- Author-owned content fields (body and optional metadata)
|
|
18
|
+
- Comment timestamps (created_at, updated_at, edited_at)
|
|
19
|
+
- Edit limit (max 5 edits) and `edited_count` for author capability display
|
|
20
|
+
- Comment ID uses ULID format (varchar(26)), same as posts
|
|
21
|
+
|
|
22
|
+
### Subject Attachment
|
|
23
|
+
|
|
24
|
+
- Every comment is attached to exactly one subject
|
|
25
|
+
- Subject is identified by `subject_type` (e.g., post, user_wall) and `subject_id`
|
|
26
|
+
- Subject cannot be null; a comment always belongs to something
|
|
27
|
+
- Domain stores only IDs for external entities (no cross-domain foreign keys)
|
|
28
|
+
|
|
29
|
+
### Reply Hierarchy
|
|
30
|
+
|
|
31
|
+
- Comments can be replies to other comments (nested structure)
|
|
32
|
+
- Top-level comments have `parent_comment_id` = null
|
|
33
|
+
- Replies have `parent_comment_id` pointing to the parent comment
|
|
34
|
+
- **Database layer**: No restriction on nesting depth
|
|
35
|
+
- **Application layer**: Maximum depth of 5 levels (configurable in code; can be increased later without schema changes)
|
|
36
|
+
- **Materialized path (`ltree`)**: Each comment stores `path` as an `ltree` built from **comment IDs** (ULIDs) as labels, plus `level` (0 = root, 1 = first reply, …) for depth checks. Used for ancestor/descendant and subtree queries, not for display order.
|
|
37
|
+
|
|
38
|
+
### Comment Lifecycle
|
|
39
|
+
|
|
40
|
+
- Soft-delete state for comment lifecycle management
|
|
41
|
+
- Readability/editability rules based on state
|
|
42
|
+
|
|
43
|
+
### Media Attachments
|
|
44
|
+
|
|
45
|
+
- Comment-linked media metadata for images and videos
|
|
46
|
+
- Attachment ordering and per-comment attachment limits
|
|
47
|
+
- Media state lifecycle (processing, ready, failed, removed)
|
|
48
|
+
|
|
49
|
+
### Comment Likes
|
|
50
|
+
|
|
51
|
+
- User-to-comment like relationships
|
|
52
|
+
- Idempotent like/unlike behavior as relationship state
|
|
53
|
+
- Aggregate like counts derived from comment_like rows
|
|
54
|
+
|
|
55
|
+
### Engagement Analytics
|
|
56
|
+
|
|
57
|
+
- Comment engagement counters and derived analytics (likes, replies, reports)
|
|
58
|
+
- Aggregation strategy for comment metrics (similar to post_stats)
|
|
59
|
+
- Retrieval of comment-level stats for display and sorting
|
|
60
|
+
|
|
61
|
+
### Post `comment_count` (read by post domain)
|
|
62
|
+
|
|
63
|
+
For `subject_type = post`, **`post.post_stats.comment_count`** is derived in **`aggregate_post_stats`** using **`select_active_comment_count_for_post`**: count **`active`** comments where **no ancestor on that post** has **`deleted`** status, using **`ltree` `path`** and the `@>` ancestor operator. A soft-deleted comment therefore removes itself and **all descendants** from that tally until counts are recomputed. See `docs/flows/posts/post_stats_aggregation.md` (Comment count semantics).
|
|
64
|
+
|
|
65
|
+
## Does Not Own
|
|
66
|
+
|
|
67
|
+
- User identity, authentication, or profile data
|
|
68
|
+
- User relationship graph (follows/friends/blocks)
|
|
69
|
+
- Subject entities (posts, user walls, etc.)—comment domain stores subject_type and subject_id only
|
|
70
|
+
- Comment reports (owned by moderation domain)
|
|
71
|
+
- Enforcement decisions (ban/mute/warn) and appeal lifecycle—owned by moderation domain
|
|
72
|
+
- Hashtags (comments do not support hashtags)
|
|
73
|
+
|
|
74
|
+
## User removal and redacted authors (design)
|
|
75
|
+
|
|
76
|
+
When an account is removed, **comments authored by that user are not hard-deleted**. The row and `id` stay so **replies and `ltree` structure remain valid** and other participants can still read the thread.
|
|
77
|
+
|
|
78
|
+
- Set `author_id` to a **well-known sentinel** (e.g. `DELETED`) defined in application/domain code; it must not match a real user id.
|
|
79
|
+
- **Redact** `content` to an agreed placeholder; the comment remains **visible** in listings where policy allows (e.g. not soft-deleted).
|
|
80
|
+
- **Moderation**: keep existing `comment_reports` and `comment_likes`; do **not** rewrite `reporter_id` / `liker_id` to the sentinel (orphan UUIDs are fine for current product). Public comment **GET** responses set **`engagement_allowed`** to `false` when `author_id` is the redaction sentinel so clients can hide like/report/reply affordances; **write** endpoints do not enforce that flag.
|
|
81
|
+
- **User identity**: resolve the sentinel to a default identity in read paths (same idea as “deleted user” for missing users).
|
|
82
|
+
|
|
83
|
+
See `docs/flows/users/user_removal.md` for the full cross-domain policy.
|
|
84
|
+
|
|
85
|
+
## Hard delete when a post is removed (design)
|
|
86
|
+
|
|
87
|
+
When an admin **hard-deletes a post** (`DELETE /admin/posts/{post_id}`), **all comments** with `subject_type = post` and `subject_id = that post` are **removed from the database** (roots and nested replies), together with **`comment_likes`**, **`comment_stats`**, **`comment_stats_dirty`**, **`comment_attachments`**, and **`comment_reports`** for those comments. This is **not** redaction: comment ids under that post cease to exist after a successful hard delete.
|
|
88
|
+
|
|
89
|
+
User **account removal** does **not** do this; it redacts authored comments in place and keeps ids for thread stability. See `docs/flows/posts/admin_posts.md` (Hard delete post) and `docs/flows/users/user_removal.md`.
|
|
90
|
+
|
|
91
|
+
## Invariants
|
|
92
|
+
|
|
93
|
+
- A comment has exactly one author_id
|
|
94
|
+
- A comment has exactly one subject (subject_type + subject_id, both not null)
|
|
95
|
+
- A comment in deleted state is not editable
|
|
96
|
+
- Attachments are immutable once published on an active comment
|
|
97
|
+
- Attachment type must be image or video
|
|
98
|
+
- A user can like a given comment at most once
|
|
99
|
+
- Engagement counters are non-negative
|
|
100
|
+
- Domain stores only IDs for external entities (no cross-domain foreign keys)
|
|
101
|
+
- Comment reads/writes only touch the comment schema
|
|
102
|
+
|
|
103
|
+
## Feed Visibility Filtering Rules
|
|
104
|
+
|
|
105
|
+
- Comment domain may keep local lookup mirrors for cross-domain visibility checks:
|
|
106
|
+
- `user_restrictions_lookup` (restricted authors: muted/banned)
|
|
107
|
+
- `user_blocks_lookup` (directed block edge: blocker -> blocked)
|
|
108
|
+
- For viewer `A` and comment author `B`, hide the comment when either:
|
|
109
|
+
- `A` has blocked `B` (exists in `user_blocks_lookup`)
|
|
110
|
+
- `B` is restricted as muted/banned (exists in `user_restrictions_lookup`)
|
|
111
|
+
- Special rule: if viewer `A` is muted, `A` can still see their own comments.
|
|
112
|
+
- These lookup tables are mirrors for read-time filtering only; source-of-truth remains outside comment domain.
|
|
113
|
+
|
|
114
|
+
## Lookup Sync Contract
|
|
115
|
+
|
|
116
|
+
- Source-of-truth for restriction state is the moderation domain.
|
|
117
|
+
- Source-of-truth for user-to-user blocks is the user domain.
|
|
118
|
+
- Comment domain mirrors (`user_restrictions_lookup`, `user_blocks_lookup`) exist only to support read-time filtering.
|
|
119
|
+
- Sync operations should be idempotent and safe to run repeatedly.
|
|
120
|
+
- No-op moderation operations (for example, banning an already banned user or clearing an already active user)
|
|
121
|
+
should still reconcile comment-domain lookup rows to heal prior cross-domain partial failures.
|
|
122
|
+
|
|
123
|
+
## Materialized Path (`ltree` + `level`)
|
|
124
|
+
|
|
125
|
+
To avoid recursive CTEs or multiple round-trips when resolving comment trees, each comment stores:
|
|
126
|
+
|
|
127
|
+
- **path** (`ltree`): A dot-separated chain of **labels**, where each label is that comment’s **own** `id` (ULID) from the root down to this node.
|
|
128
|
+
- **Root**: `path` is a single label—the new comment’s `id` (e.g. `01HZXK...` as one `ltree` value).
|
|
129
|
+
- **Reply**: `path` is `parent.path` with this comment’s `id` appended as the next label (e.g. `01HZROOT...01HZCHILD...` in `ltree` form).
|
|
130
|
+
- Stored type is PostgreSQL `ltree`; the `ltree` extension lives in the shared **extension** schema (see repo Postgres conventions). App connections must resolve `ltree` types/operators (e.g. via `search_path` including the extension schema).
|
|
131
|
+
- **level** (int): Depth in the tree (0 = root, 1 = first reply, …). Used for max-depth enforcement and API rules (e.g. `can_reply`). May be kept in sync with `nlevel(path) - 1` conceptually.
|
|
132
|
+
|
|
133
|
+
**Display order**: List and reply endpoints use **`created_at` (or other explicit sort keys)**. Do not use `ORDER BY path` for Reddit-style chronological or scored ordering—`ltree` compares labels as text; ULID labels are not a reliable sibling order.
|
|
134
|
+
|
|
135
|
+
With **max depth 5**, paths are short (at most five labels, each 26 characters for a ULID), which keeps rows and indexes reasonable.
|
|
136
|
+
|
|
137
|
+
### Example
|
|
138
|
+
|
|
139
|
+
Illustrative (real IDs are 26-character ULIDs):
|
|
140
|
+
|
|
141
|
+
| comment_id | parent_comment_id | path (`ltree`) | level |
|
|
142
|
+
| ---------- | ----------------- | ---------------------- | ----- |
|
|
143
|
+
| `01HZ…A` | null | `01HZ…A` | 0 |
|
|
144
|
+
| `01HZ…B` | `01HZ…A` | `01HZ…A.01HZ…B` | 1 |
|
|
145
|
+
| `01HZ…C` | `01HZ…A` | `01HZ…A.01HZ…C` | 1 |
|
|
146
|
+
| `01HZ…D` | `01HZ…B` | `01HZ…A.01HZ…B.01HZ…D` | 2 |
|
|
147
|
+
|
|
148
|
+
### Query patterns
|
|
149
|
+
|
|
150
|
+
- **Subtree (descendants of a node, including the node)**: `WHERE path <@ root.path` (and usually restrict by `subject_type` / `subject_id` for safety).
|
|
151
|
+
- **Ancestor chain**: walk `parent_comment_id` or use `ltree` operators / `subpath` as needed.
|
|
152
|
+
- **Depth limit**: `WHERE level < 5` (application max depth); or `nlevel(path) <= 5` if aligned with the same policy.
|
|
153
|
+
- **Indexing**: GiST on `path` supports `<@`, `@>`, and related `ltree` operators (see PostgreSQL `ltree` docs).
|
|
154
|
+
|
|
155
|
+
### Insert behavior
|
|
156
|
+
|
|
157
|
+
1. Generate the new comment `id` (ULID) before insert.
|
|
158
|
+
1. **Root**: set `path` to that single id as `ltree`; set `level` to 0.
|
|
159
|
+
1. **Reply**: read parent’s `path` and `level`; set `path` to `parent.path || new_id`; set `level` to `parent.level + 1`.
|
|
160
|
+
|
|
161
|
+
No sibling index or `count(*)` is required for `path` construction—each new segment is unique because the new `id` is unique.
|
|
162
|
+
|
|
163
|
+
### Path format (summary)
|
|
164
|
+
|
|
165
|
+
- **Chosen format**: ID labels only (`comment_id` per segment).
|
|
166
|
+
- **Not used**: Numeric sibling indices (`1.2.3`) for `path`; those are superseded by this design.
|
|
167
|
+
|
|
168
|
+
## Nesting Depth Policy
|
|
169
|
+
|
|
170
|
+
- **Database**: No constraint on reply depth. The schema allows arbitrary nesting via `parent_comment_id`.
|
|
171
|
+
- **Application**: Enforce a maximum depth of 5 levels when creating replies. This limit lives in the application/service layer, not in the database.
|
|
172
|
+
- **Extensibility**: To increase the nesting limit, change the application-layer constant or configuration; no migration required.
|
|
173
|
+
|
|
174
|
+
## Persistence Model
|
|
175
|
+
|
|
176
|
+
### Tables
|
|
177
|
+
|
|
178
|
+
#### comments
|
|
179
|
+
|
|
180
|
+
- Canonical comment record (id, author_id, content, subject_type, subject_id, parent_comment_id, path, level, status, timestamps)
|
|
181
|
+
- `id`: ULID format (varchar(26)), same as posts
|
|
182
|
+
- `path`: `ltree` materialized path; each label is the comment `id` along the chain from root to this row (see Materialized Path above)
|
|
183
|
+
- `level`: Depth in reply tree (0 = root, 1 = first reply, …)
|
|
184
|
+
- Supports soft deletion and edit tracking (max 5 edits)
|
|
185
|
+
- Indexed for subject-based listing, author timeline, parent lookups, and subtree queries (e.g. GiST on `path` for `ltree` operators)
|
|
186
|
+
|
|
187
|
+
#### comment_stats
|
|
188
|
+
|
|
189
|
+
- Denormalized engagement counters per comment
|
|
190
|
+
- Stores comment_id, like_count, reply_count, report_count, timestamps
|
|
191
|
+
- like_count from comment_likes; reply_count from comments (parent_comment_id); report_count from moderation domain comment_reports
|
|
192
|
+
- Aggregated periodically via dirty-table pattern. See `docs/flows/comments/comment_stats_aggregation.md`.
|
|
193
|
+
- One row per comment; created when comment is created
|
|
194
|
+
|
|
195
|
+
#### comment_stats_dirty
|
|
196
|
+
|
|
197
|
+
- Tracks which comments need stats aggregation
|
|
198
|
+
- One row per comment_id; INSERT ON CONFLICT DO NOTHING
|
|
199
|
+
- Marked by like, reply, report events (async after commit)
|
|
200
|
+
- Drained by scheduled job every 5 min; worker aggregates and deletes
|
|
201
|
+
|
|
202
|
+
#### comment_likes
|
|
203
|
+
|
|
204
|
+
- Tracks user likes on comments as a relationship table
|
|
205
|
+
- Stores comment_id, liker_id, and created_at
|
|
206
|
+
- One row per (comment_id, liker_id) pair to enforce idempotent likes
|
|
207
|
+
|
|
208
|
+
#### user_restrictions_lookup
|
|
209
|
+
|
|
210
|
+
- Read-time mirror for restricted users (muted/banned). Source-of-truth: moderation domain.
|
|
211
|
+
- Stores user_id, restriction_type (muted, banned)
|
|
212
|
+
- Used in comment queries to hide comments from restricted authors. Sync operations reconcile from moderation domain; see Lookup Sync Contract above.
|
|
213
|
+
|
|
214
|
+
#### user_blocks_lookup
|
|
215
|
+
|
|
216
|
+
- Read-time mirror for user-to-user blocks. Source-of-truth: user domain.
|
|
217
|
+
- Stores blocker_id, blocked_id (directed edge: blocker has blocked blocked)
|
|
218
|
+
- Used in comment queries to hide comments from authors the viewer has blocked. Sync operations reconcile from user domain; see Lookup Sync Contract above.
|
|
219
|
+
|
|
220
|
+
#### comment_attachments
|
|
221
|
+
|
|
222
|
+
- Tracks media attached to comments (image/video)
|
|
223
|
+
- Stores comment_id, media_id/storage_key, media_type, order_index, and processing status
|
|
224
|
+
- Supports deterministic ordering for multi-attachment comments
|
|
225
|
+
|
|
226
|
+
### Enums
|
|
227
|
+
|
|
228
|
+
#### comment_subject_type
|
|
229
|
+
|
|
230
|
+
- Subject type for comment attachment (post, user_wall, …)
|
|
231
|
+
- Extensible: add new values as new subject types are introduced
|
|
232
|
+
|
|
233
|
+
#### comment_status
|
|
234
|
+
|
|
235
|
+
- Lifecycle state of a comment (active, deleted)
|
|
236
|
+
|
|
237
|
+
#### comment_media_type
|
|
238
|
+
|
|
239
|
+
- Attachment media type (image, video)
|
|
240
|
+
|
|
241
|
+
#### comment_attachment_status
|
|
242
|
+
|
|
243
|
+
- Attachment processing lifecycle (processing, ready, failed, removed)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from core_framework.domains.comment.enums import CommentStatus, CommentSubjectType
|
|
2
|
+
from core_framework.domains.comment.exceptions import (
|
|
3
|
+
BaseCommentException,
|
|
4
|
+
CommentEditLimitReachedException,
|
|
5
|
+
CommentNotFoundException,
|
|
6
|
+
CommentUpdateNotFoundException,
|
|
7
|
+
MaxReplyDepthException,
|
|
8
|
+
ParentCommentNotFoundException,
|
|
9
|
+
)
|
|
10
|
+
from core_framework.domains.comment.models import Comment, CommentPreview, CommentStats, CommentWithMetadata
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CommentStatus",
|
|
14
|
+
"CommentSubjectType",
|
|
15
|
+
"BaseCommentException",
|
|
16
|
+
"CommentEditLimitReachedException",
|
|
17
|
+
"CommentNotFoundException",
|
|
18
|
+
"CommentUpdateNotFoundException",
|
|
19
|
+
"MaxReplyDepthException",
|
|
20
|
+
"ParentCommentNotFoundException",
|
|
21
|
+
"Comment",
|
|
22
|
+
"CommentPreview",
|
|
23
|
+
"CommentStats",
|
|
24
|
+
"CommentWithMetadata",
|
|
25
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
|
|
2
|
+
from core_framework.domains.comment.repository import CommentRepository
|
|
3
|
+
from core_framework.domains.comment.service import CommentService
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_comment_repository(runtime: CoreRuntime) -> CommentRepository:
|
|
7
|
+
return CommentRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_comment_service(runtime: CoreRuntime) -> CommentService:
|
|
11
|
+
return CommentService(build_comment_repository(runtime))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_comment_dependencies(runtime: CoreRuntime) -> None:
|
|
15
|
+
global comment_repository, comment_service
|
|
16
|
+
comment_repository = build_comment_repository(runtime)
|
|
17
|
+
comment_service = build_comment_service(runtime)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
comment_repository = unconfigured_dependency("CommentRepository")
|
|
21
|
+
comment_service = unconfigured_dependency("CommentService")
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"build_comment_repository",
|
|
25
|
+
"build_comment_service",
|
|
26
|
+
"configure_comment_dependencies",
|
|
27
|
+
"comment_repository",
|
|
28
|
+
"comment_service",
|
|
29
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class BaseCommentException(Exception):
|
|
2
|
+
message: str
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str):
|
|
5
|
+
self.message = message
|
|
6
|
+
super().__init__(self.message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ParentCommentNotFoundException(BaseCommentException):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
super().__init__("Parent comment not found or not active")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaxReplyDepthException(BaseCommentException):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__("Maximum reply depth reached")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommentUpdateNotFoundException(BaseCommentException):
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
super().__init__("Unable to process request")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CommentEditLimitReachedException(BaseCommentException):
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__("Edit limit reached. Maximum edits allowed per comment.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommentNotFoundException(BaseCommentException):
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
super().__init__("Comment not found")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from core_framework.domains.comment.enums import CommentStatus, CommentSubjectType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
9
|
+
class CommentStats:
|
|
10
|
+
like_count: int
|
|
11
|
+
reply_count: int
|
|
12
|
+
report_count: int
|
|
13
|
+
|
|
14
|
+
DEFAULT: ClassVar[CommentStats]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
18
|
+
class Comment:
|
|
19
|
+
id: str
|
|
20
|
+
author_id: str
|
|
21
|
+
content: str
|
|
22
|
+
edited_count: int
|
|
23
|
+
edited_at: datetime | None
|
|
24
|
+
level: int
|
|
25
|
+
created_at: datetime
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
29
|
+
class CommentWithMetadata:
|
|
30
|
+
id: str
|
|
31
|
+
author_id: str
|
|
32
|
+
content: str
|
|
33
|
+
subject_type: CommentSubjectType
|
|
34
|
+
subject_id: str
|
|
35
|
+
parent_comment_id: str | None
|
|
36
|
+
status: CommentStatus
|
|
37
|
+
edited_count: int
|
|
38
|
+
edited_at: datetime | None
|
|
39
|
+
created_at: datetime
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
43
|
+
class CommentPreview:
|
|
44
|
+
id: str
|
|
45
|
+
author_id: str
|
|
46
|
+
content: str
|
|
47
|
+
status: CommentStatus
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CommentStats.DEFAULT = CommentStats(
|
|
51
|
+
like_count=0,
|
|
52
|
+
reply_count=0,
|
|
53
|
+
report_count=0,
|
|
54
|
+
)
|