Contributor
Purpose
TBD by human
Identity & key fields
- Primary key:
id(uuid, defaultgen_random_uuid()). activityId(uuid, FK →activities.activities.id, on-delete cascade).memberId(uuid, FK →companies.company_member.id, on-delete cascade) — contributors are always backed by acompanyMembersrow, see ADR contributors-must-be-members (v4) and ADR global-user-identity D9.roles(array of enumcontributor_role:ACTOR,DIRECTOR,SPEAKER,COACH,HOST) — non-empty.order(integer, default0).
business meaning: TBD by human
Invariants
memberIdis NOT NULL — every contributor IS acompanyMembersrow (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).- UNIQUE constraint
contributors_activity_id_member_id_uniqueon(activityId, memberId)— 1:1 mapping between activity and member (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts). - CHECK constraint
contributors_roles_not_empty—array_length(roles, 1) >= 1(enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts). - Both FKs use ON DELETE CASCADE (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
- The schema comment explicitly notes that
personName, the singularrole,description, andavatarUrlcolumns are intentionally absent — see ADR contributors-must-be-members. - Identity reads via
member.user.*andmember.user.publicProfile.*(per ADR contributors-must-be-members v4, amended by ADR global-user-identity D9). The previous “every name/avatar/bio render goes throughmember.*” rule is updated to “throughmember.user.*andmember.user.publicProfile.*”. Reads cross the joincontributor → companyMember → users → user_public_profile— projection is inlibs/features/activities/src/lib/services/activity-contributors.service.ts.
business invariants: TBD by human
Lifecycle
No explicit lifecycle / status column.
Relationships
- Activity (ENT-005) —
activityId→activities.activities.id, on-delete cascade. N:1. - Company member (ENT-019) —
memberId→companies.company_member.id, on-delete cascade. N:1. The contributor → member FK with cascade still stands; v4 of the contributors ADR only changes the read path, not the graph topology. - User (ENT-021) — read-only join through
member.user. Source ofglobalNameandavatarUrlfor renders. No FK on this table. - User public profile (ENT-042) — read-only join through
member.user.publicProfile. Source ofbio,specializations,links,slug,verifiedAt,coverPhotoUrl. No FK on this table.
API surfaces
| Surface | Exposed | Notes |
|---|---|---|
| client | yes — ContributorPreviewDto, ContributorMemberPreviewDto referenced from activity detail. Field-stable after the v4 ADR — keys publicName, avatarUrl, bio, specializations, links preserved; values now projected from users.* and user_public_profile.*. Ratings addition (AC-9): ContributorPreviewDto.ratingSummary?: RatingSummaryPreviewDto added — optional, absent for offline contributors (member.userId IS NULL); see Contributor review. Search (Phase C): a contributor’s global identity (member.user.id = users.id) is the key for contributor-aware activity search (?contributorUserId=) and session filter; users.full_name (globalName) is matched in search Tier B via pg_trgm. See Activity API surfaces and ADR contributor-aware-search-and-session-filter. Autocomplete endpoint (trainer filter): GET /api/client/contributors (operationId: contributorsClientList) returns ContributorAutocompleteItemDto[] = { userId, publicName, avatarUrl } — ranked by future-session count (empty q) or pg_trgm / ILIKE name match (non-empty q). Scope: published contributors only. Auth: public. See ADR activities-trainer-autocomplete. | Swagger UI |
| business | yes — /activities/{activityId}/contributors, /activities/{activityId}/contributors/{contributorId}, CreateContributorDto, UpdateContributorDto, ContributorMemberPreviewAdminDto. Restructured — identity nested under user.globalName, user.avatarUrl, user.publicProfile.*. Autocomplete endpoint (trainer filter): GET /api/business/contributors (operationId: contributorsAdminList) returns the same ContributorAutocompleteItemDto[] shape, scoped to the active company (from @ActiveCompany() header via CompanyRolesGuard). Auth: JWT + READ_ACTIVITIES. Scope: contributors on any activity of the active company regardless of publication status. See ADR activities-trainer-autocomplete. | Swagger UI |
| super-admin | no | — |
The client surface exposes display-oriented previews (flat shape preserved for zero template churn); the business surface exposes write operations and the admin-only nested member preview shape. Per-surface divergence is intentional — see ADR global-user-identity D9 “Field-shape per surface”.
Known gotchas / open questions
- Per schema comment, the model is “members-only”: there is no support for guest / external contributors — they would have to be created as
companyMembersfirst. - Roles array is intentionally an array (the singular
rolewas dropped) — multi-role contributors are explicit. - Identity reads cross-context. After ADR contributors-must-be-members v4 (amended by ADR global-user-identity D9), the identity read path is
contributor → member → user → user_public_profile. The four-step join is encapsulated inactivity-contributors.service.ts; consumers should not re-invent it. - Client vs business DTO divergence. The client-surface preview keeps flat field names (
publicName,bio, …); the business-surface preview nests them underuser.publicProfile.*. Same logical entity, two intentional JSON shapes — per the “never share types between surfaces” rule.