Skip to content

feat(server): Stage 1 brand-domain resolver (#4159)#4299

Merged
bokelley merged 2 commits intomainfrom
bokelley/4159-stage1-resolver
May 9, 2026
Merged

feat(server): Stage 1 brand-domain resolver (#4159)#4299
bokelley merged 2 commits intomainfrom
bokelley/4159-stage1-resolver

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 9, 2026

Summary

Stage 1 of #4159 (spec at specs/domain-column-rationalization.md). Introduces the single read surface for an org's brand-identity primary domain.

import { getBrandPrimaryDomain, getBrandPrimaryDomainsForOrgs } from '../services/brand-domain-resolver.js';

const domain = await getBrandPrimaryDomain(orgId); // string | null
const map = await getBrandPrimaryDomainsForOrgs(orgIds); // Map<orgId, domain>

Read order:

  1. organization_domains.is_primary=true (Stage 1 canonical, post-Stage-0 backfill)
  2. member_profiles.primary_brand_domain (transition fallback; logs a warn on every hit so we can spot drift)

Once every read site has migrated to the resolver, Stage 2 drops the column and the fallback becomes a no-op (and gets removed). Stage 3 introduces the matching setPrimaryDomain writer.

What's here

  • server/src/services/brand-domain-resolver.ts — the resolver. Two surfaces (single + batched).
  • server/tests/integration/brand-domain-resolver.test.ts — 9 cases covering primary-wins, fallback path, null/empty, mixed batches.
  • Changeset.

What's NOT here

The ~14 call sites that read member_profiles.primary_brand_domain directly. Those migrate in subsequent PRs — splitting them keeps each rewrite small and reviewable. Nothing breaks during the gap because writers continue to dual-write.

Survey shows fallback should be ~empty in prod

Post-Stage-0 (this thread): 91 of 158 profiles have brand_primary == organization_domains.is_primary, 66 are NULL, 1 (HYPD orphan) is divergent. So the fallback path will fire only on:

  • The HYPD orphan personal workspace (known cross-org collision)
  • Any orgs that join after the backfill ran without going through the auto-populate webhook

Both are watchable signals via the warn log.

Test plan

  • 9 integration tests pass
  • Typecheck clean
  • After merge: monitor getBrandPrimaryDomain fell back warns to spot any drift; should be near-zero

🤖 Generated with Claude Code

Introduces the single read surface for an org's brand-identity primary
domain. Reads organization_domains.is_primary=true first (the canonical
Stage 1 source after Stage 0's backfill), falls back to
member_profiles.primary_brand_domain for any org Stage 0 missed. Logs
a warn on every fallback so we can spot remaining drift before Stage 2
drops the column.

Two surfaces:
- getBrandPrimaryDomain(orgId): single-org lookup
- getBrandPrimaryDomainsForOrgs(orgIds): batched, for list views

No call site changes here. Subsequent PRs migrate the ~14 read sites
identified in the spec.

Spec: specs/domain-column-rationalization.md (#4215).

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

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { initializeDatabase, getPool, closeDatabase } from '../../src/db/client.js';
Code-reviewer:
- Refactor test setup so beforeAll/afterAll runs once per file (was once
  per describe — duplicate runMigrations + double pool init).
- Aggregate batch fallback warns into one log line per call instead of
  one-per-row. Keeps signal usable on large batches.
- Add tests for verified=false is_primary=true (documents that the
  resolver doesn't filter on verified — verified is enforced at write
  time) and multiple-is_primary=true rows (documents the don't-crash
  contract on data anomalies).
- Tighten doc comment: missing orgs are absent from the result map,
  not present with undefined values.

Security-reviewer:
- Add explicit AUTHORIZATION note to the file's doc comment.
- Replace LIMIT 1 (without ORDER BY) with full-result + multi-primary
  detection. logger.error if the Stage 0 "exactly one" invariant is
  broken; still return a valid primary so callers don't crash on the
  anomaly. Same shape applied to the batch variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 763cded into main May 9, 2026
13 checks passed
@bokelley bokelley deleted the bokelley/4159-stage1-resolver branch May 9, 2026 16:37
bokelley added a commit that referenced this pull request May 9, 2026
….3) (#4303)

* feat(http): dashboard + brand-feeds reads use resolver (#4159 Stage 1.3)

Migrates four read sites:
- GET /api/members: batched resolver lookup; brand-primary keyed by
  org_id rather than per-row column.
- GET /api/members/carousel: same batched pattern.
- GET /api/members/:slug: single-org resolver.
- routes/brand-feeds.ts ownership check: replaces the member_profiles
  query in the owned-domains union with the resolver. orgDomains query
  unchanged (still walks all verified rows).

The list endpoints are the first real users of the batched resolver
variant getBrandPrimaryDomainsForOrgs from PR #4299. Behavior unchanged
post-Stage-0 — brand-primary now keys on organization_domains.is_primary
canonically with member_profiles fallback for orgs the backfill missed.

97 tests pass across the 6 affected test files.

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

* address Stage 1.3 review nits

- Tighten the batched-resolver comment in /api/members (was 'three
  queries', should be 'constant number'; resolver itself can do 1-2).
- Trim the brand-feeds ownership-gate comment.
- TODO(#4159) noting the pre-existing verified-filter gap on the
  ownership gate. Set-equivalent to before this PR; worth fixing in
  Stage 2 when the column drops and the gate logic gets revisited.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant