feat(api): link a Job/Contract's employer to a network Organization (#375) #376

Merged
james merged 1 commit from 375-job-org-link-backend into main 2026-06-29 16:34:08 +00:00
Owner

Backend for #375 — tie a company you've worked at to a tracked network Organization, mirroring the people↔organizations link-or-stub pattern. A job is 1:1 with an employer, so it's a nullable FK column on jobs, not a junction. The client UI (link picker + "your roles here") is a tracked follow-up.

What's in it

  • Migration 026: jobs.organization_id nullable FK → organizations(id) ON DELETE SET NULL. Existing jobs stay unlinked free-text — no backfill.
  • Live name on read: linked jobs resolve the org's current name via LEFT JOIN organizations (JobWithOrg → DTO organizationName); rename the org → the job reflects it. company is kept as a fallback snapshot, refreshed to the org name at link time.
  • Org delete = copy-down (OrganizationsRepository.deleteById): snapshot the org name into linked jobs' company + null the link before deleting the org, so career history degrades to a stub with no data loss. The FK SET NULL is the Postgres safety net; the app-level copy-down is the real mechanism (SQLite FK enforcement is off) and runs on both engines — two plain statements (no transaction, mirroring conversations.delete for the libsql :memory: leg).
  • Link validation (lib/api/job-organization-link.ts): a cross-user/missing org id → 404 (don't leak existence); applies to both jobs and contracts. On a valid link, company is set to the org's name (fresh snapshot).
  • Reverse view: GET /api/organizations/{id}/roles lists the linked jobs/roles.
  • zod/openapi + api-client regenerated (coverage 121 → 122); idea.md updated.

Verification (I re-ran all of it, incl. the Postgres leg)

  • test against both engines (ephemeral Postgres): 1257 passed, 0 skipped — the migration applies on Postgres and the new dual-engine tests cover link, unlink, live-name-on-rename, org-delete copy-down, listByOrganizationId scoping, cross-user 404, and contract linking.
  • typecheck ✓ · lint ✓ · openapi:check ✓ / coverage 122 · @carol/api-client check ✓ · semgrep ✓ 0 findings on the new files.

Follow-up (rest of #375)

  • Client: Job create/edit company field that picks an Organization or stays free text; tap-company → Org detail; the org detail's "your roles here" section (consumes /roles).
  • An agent link_job_to_org tool (mirroring link_person_to_org).

Part of #375.

🤖 Generated with Claude Code

Backend for #375 — tie a company you've worked at to a tracked network Organization, mirroring the people↔organizations link-or-stub pattern. A job is 1:1 with an employer, so it's a nullable FK column on `jobs`, not a junction. **The client UI (link picker + "your roles here") is a tracked follow-up.** ## What's in it - **Migration 026:** `jobs.organization_id` nullable FK → `organizations(id)` `ON DELETE SET NULL`. Existing jobs stay unlinked free-text — no backfill. - **Live name on read:** linked jobs resolve the org's current name via `LEFT JOIN organizations` (`JobWithOrg` → DTO `organizationName`); rename the org → the job reflects it. `company` is kept as a fallback snapshot, refreshed to the org name at link time. - **Org delete = copy-down** (`OrganizationsRepository.deleteById`): snapshot the org name into linked jobs' `company` + null the link *before* deleting the org, so career history degrades to a stub with no data loss. The FK `SET NULL` is the Postgres safety net; the app-level copy-down is the real mechanism (SQLite FK enforcement is off) and runs on both engines — two plain statements (no transaction, mirroring `conversations.delete` for the libsql `:memory:` leg). - **Link validation** (`lib/api/job-organization-link.ts`): a cross-user/missing org id → **404** (don't leak existence); applies to both jobs and contracts. On a valid link, `company` is set to the org's name (fresh snapshot). - **Reverse view:** `GET /api/organizations/{id}/roles` lists the linked jobs/roles. - zod/openapi + api-client regenerated (coverage 121 → **122**); `idea.md` updated. ## Verification (I re-ran all of it, incl. the Postgres leg) - `test` against **both engines** (ephemeral Postgres): **1257 passed, 0 skipped** — the migration applies on Postgres and the new dual-engine tests cover link, unlink, **live-name-on-rename**, **org-delete copy-down**, `listByOrganizationId` scoping, **cross-user 404**, and contract linking. - `typecheck` ✓ · `lint` ✓ · `openapi:check` ✓ / coverage 122 · `@carol/api-client check` ✓ · semgrep ✓ 0 findings on the new files. ## Follow-up (rest of #375) - Client: Job create/edit company field that picks an Organization or stays free text; tap-company → Org detail; the org detail's "your roles here" section (consumes `/roles`). - An agent `link_job_to_org` tool (mirroring `link_person_to_org`). Part of #375. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): link a Job/Contract's employer to a network Organization (#375)
Some checks failed
PR / Trivy (image) (pull_request) Successful in 3m33s
PR / E2E (Playwright) (pull_request) Failing after 8m13s
Commits / Conventional Commits (pull_request) Successful in 8s
PR / Static analysis (pull_request) Successful in 2m21s
PR / pnpm audit (pull_request) Successful in 2m32s
PR / OpenAPI (pull_request) Successful in 3m7s
PR / Lint (pull_request) Successful in 3m24s
PR / Client (web export smoke) (pull_request) Successful in 3m37s
PR / Typecheck (pull_request) Successful in 3m48s
PR / Build (pull_request) Successful in 4m11s
PR / Package age policy (soft) (pull_request) Successful in 46s
PR / Test (sqlite) (pull_request) Successful in 4m21s
PR / OSV-Scanner (pull_request) Successful in 2m5s
Secrets / gitleaks (pull_request) Successful in 48s
PR / Test (postgres) (pull_request) Successful in 4m26s
PR / Coverage (soft) (pull_request) Successful in 2m35s
9f79621dcf
Backend for tying a company you've worked at to a tracked Organization,
mirroring the people↔organizations link-or-stub pattern. A job is 1:1
with an employer, so it's a nullable FK column on jobs, not a junction.

- Migration 026: jobs.organization_id nullable FK → organizations(id)
  ON DELETE SET NULL. Existing jobs stay unlinked free-text — no backfill.
- Linked jobs resolve the org's LIVE name on read (LEFT JOIN organizations
  → JobWithOrg / DTO organizationName); company is kept as a fallback
  snapshot, refreshed to the org's name at link time.
- Org delete = copy-down (OrganizationsRepository.deleteById): snapshot
  the org name into linked jobs' company + null the link before deleting,
  so career history degrades to a stub with no data loss. FK SET NULL is
  the Postgres safety net; the app-level copy-down is the real mechanism
  (SQLite FK enforcement is off), runs on both engines.
- Link validation (lib/api/job-organization-link.ts): a cross-user/missing
  org id → 404 (don't leak existence); applies to both jobs and contracts.
- Reverse view: GET /api/organizations/{id}/roles lists the linked
  jobs/roles (for the org detail's "your roles here").
- zod/openapi + api-client regenerated (coverage 121 → 122); idea.md
  updated.

Dual-engine tests: link, unlink, live-name-on-rename, org-delete copy-down,
listByOrganizationId scoping, cross-user 404, contract linking.

Part of #375 (client UI — link picker + "roles here" — is a follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.5% ≥ 50%
Branches 71.5% ⚠️ ≥ 75%
Functions 80.6% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.5% ✅ | ≥ 50% | | Branches | 71.5% ⚠️ | ≥ 75% | | Functions | 80.6% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 25fe865651 into main 2026-06-29 16:34:08 +00:00
james deleted branch 375-job-org-link-backend 2026-06-29 16:34:08 +00:00
Sign in to join this conversation.
No description provided.