feat(api+client): organizations feature with links and key people (#28) #269
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!269
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "28-organizations-feature"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Expands the minimal Organizations skeleton from #27 into the full #28 surface. Adds
description+date_joinedcolumns to the existingorganizationstable, two child tables (organization_links,organization_key_people), the matching CRUD routes + DTOs + TanStack hooks, and a new Organization detail screen at/network/orgs/[id]. The/networkscreen restructures into People / Organizations tabs.Schema diff
Migration
019_organizations_expand.ts:organizations— adddescription TEXT NULL,date_joined TEXT NULL(ISOYYYY-MM-DD).organization_links— new table:id,user_id(FK users),organization_id(FK organizations CASCADE),url,label?,display_order,created_at. Index(user_id, organization_id, display_order).organization_key_people— new table:id,user_id(FK users),organization_id(FK organizations CASCADE),person_id(FK people ON DELETE CASCADE — implements the acceptance criterion),role?,display_order,created_at. UNIQUE(user_id, organization_id, person_id). Indexes on(user_id, organization_id, display_order)+(person_id)(for the reverse cascade lookup).apps/api/tests/db/_engines.tsKYSELY_TABLESupdated in dependency order (#259 trap — children before parents).Route table
/api/organizationslinkCount+keyPeopleCount./api/organizationsdescription,dateJoined./api/organizations/{id}personName)./api/organizations/{id}/api/organizations/{id}/api/organizations/{id}/linksz.url()+ refine./api/organizations/{id}/links/{linkId}/api/organizations/{id}/links/{linkId}/api/organizations/{id}/key-peoplekey_person_existson UNIQUE pre-check./api/organizations/{id}/key-people/{entryId}/api/organizations/{id}/key-people/{entryId}All routes use
getAuthIdentity(req)(session OR bearer) and return 404 on cross-user. OpenAPI coverage: 98 (path, method) pairs registered; drift gate green.Network UX decision (Option A vs B)
Picked A. Segmented control on
/networkwith People + Organizations tabs, pure client-state, no sub-routes — mirrors the Experience screen's tab pattern (Education / Jobs / Contracts). Lighter footprint than sub-routes, keeps the existing "add a person" CTA on the primary/networklanding, and matches what the rest of the app does. Option B (/network/people+/network/organizations) would force every Network nav click to land on a redirect.The Organization detail screen lives at a sub-route —
/network/orgs/[id]— so it can have its own page-level Edit toggle, mirroring the/network/[id]Person detail screen and the #266 Profile pattern.People-detail cross-link
The Person detail screen's organisation list now taps through to
/network/orgs/[id]for linked-form rows (the existingorganizationIdfrom #27). Stub-form rows stay non-interactive — they don't reference a tracked Organization.Decisions worth flagging
z.url()plus a refine requiringhttp(s)://. This rejectsjavascript:and other surprises. The client UI also runs the regex before submitting, so the typical "oops I typedexample.com" never round-trips a 400.organization_key_peoplerow on every Organization — DDL-levelON DELETE CASCADEonperson_id. Considered "leave as orphan withperson_idNULL" but rejected: that'd leak a tombstone row through the API, and the Organization screen would renderpersonName: "?"indefinitely with no recovery path.Test plan
_engines.tsmatrix whenTEST_POSTGRES_URLis set).javascript:), 409key_person_exists, and the end-to-end "delete a Person → key-people row is gone, Organization survives" cascade through the API.pnpm -F @carol/api typecheck / lint / test / openapi:check / openapi:coverage— all green.pnpm -F @carol/api-client typecheck / lint / test / check— generated schema regenerated; drift gate green.pnpm -F @carol/client typecheck / lint / test / export:web— all green; new/network/orgs/[id]route exported.Surprises
zOrganizationDtohad become a backwards-compat re-export of the new summary shape; the named OpenAPI componentOrganizationDtonow points at the same z schema asOrganizationSummaryDto. Both names appear in the spec so future codegen consumers can move at their own pace.loadOwnedOrganizationhelper mirrorsloadOwnedPersonline-for-line — the People pattern translated cleanly. No surprises in the sub-resource routes.Links
Sibling: #27 (People). Spec:
idea.md→ Network → Organizations.Closes #28.
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.