From e4d39fd696a54151afeabd93334d798fd388b46c Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 9 May 2026 13:53:17 -0400 Subject: [PATCH 1/2] feat(http): dashboard + brand-feeds reads use resolver (#4159 Stage 1.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../stage1-3-dashboard-feeds-resolver.md | 4 ++ server/src/http.ts | 41 +++++++++++-------- server/src/routes/brand-feeds.ts | 13 +++--- 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 .changeset/stage1-3-dashboard-feeds-resolver.md diff --git a/.changeset/stage1-3-dashboard-feeds-resolver.md b/.changeset/stage1-3-dashboard-feeds-resolver.md new file mode 100644 index 0000000000..96a0b212b5 --- /dev/null +++ b/.changeset/stage1-3-dashboard-feeds-resolver.md @@ -0,0 +1,4 @@ +--- +--- + +Stage 1.3 of #4159: migrate the public member-list endpoints (`/api/members`, `/api/members/carousel`, `/api/members/:slug` in `server/src/http.ts`) and the brand-feeds ownership check (`server/src/routes/brand-feeds.ts`) from direct `primary_brand_domain` reads to `getBrandPrimaryDomain[sForOrgs]`. List endpoints exercise the batched variant introduced in PR #4299. No behavioral change post-Stage-0; brand-primary now resolves from `organization_domains.is_primary` (canonical) with `member_profiles` fallback. diff --git a/server/src/http.ts b/server/src/http.ts index f2338dc195..94202a58f6 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -37,6 +37,7 @@ import Stripe from "stripe"; import { OrganizationDatabase, getUserSeatType, buildSubscriptionUpdate, TIER_PRESERVING_STATUSES, type SeatType, type MembershipTier } from "./db/organization-db.js"; import { MemberDatabase } from "./db/member-db.js"; import { ensureMemberProfilePublished } from "./services/member-profile-autopublish.js"; +import { getBrandPrimaryDomain, getBrandPrimaryDomainsForOrgs } from "./services/brand-domain-resolver.js"; import { getGitHubConnectedAccount, getGitHubAuthorizeUrl, disconnectGitHub, buildPipesReturnTo } from "./services/pipes.js"; import { BrandDatabase, resolveBrandFromJson } from "./db/brand-db.js"; import { CatalogEventsDatabase } from "./db/catalog-events-db.js"; @@ -8309,11 +8310,12 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< offset: offset ? parseInt(offset as string, 10) : 0, }); - // Batch-fetch all brand data and credentials in two queries instead of N+1 - const brandDomains = profiles - .map(p => p.primary_brand_domain) - .filter((d): d is string => !!d); + // Batch-fetch all brand data and credentials in three queries instead of N+1. + // Brand-primary domains come from the Stage 1 resolver (org_domains.is_primary + // first, member_profiles fallback), keyed by org_id rather than a per-row column. const orgIds = profiles.map(p => p.workos_organization_id); + const brandPrimaryByOrg = await getBrandPrimaryDomainsForOrgs(orgIds); + const brandDomains = Array.from(brandPrimaryByOrg.values()); const [brandsMap, credentialsMap] = await Promise.all([ this.brandDb.getDiscoveredBrandsByDomains(brandDomains), @@ -8323,11 +8325,12 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< ]); for (const profile of profiles) { - if (profile.primary_brand_domain) { - const brand = brandsMap.get(profile.primary_brand_domain.toLowerCase()); + const brandPrimaryDomain = brandPrimaryByOrg.get(profile.workos_organization_id); + if (brandPrimaryDomain) { + const brand = brandsMap.get(brandPrimaryDomain.toLowerCase()); if (brand?.brand_manifest) { profile.resolved_brand = resolveBrandFromJson( - profile.primary_brand_domain, + brandPrimaryDomain, brand.brand_manifest as Record, brand.domain_verified ?? false ); @@ -8353,19 +8356,22 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< try { const profiles = await memberDb.getCarouselProfiles(); - // Batch-fetch all brand data in a single query to avoid pool exhaustion + // Batch-fetch all brand data in two queries to avoid pool exhaustion. + // Brand-primary domains come from the Stage 1 resolver (org_domains.is_primary + // first, member_profiles fallback), keyed by org_id. // codeql[js/user-controlled-bypass] - brand domains come from server-side DB, not user input - const brandDomains = profiles - .map(p => p.primary_brand_domain) - .filter((d): d is string => !!d); + const orgIds = profiles.map(p => p.workos_organization_id); + const brandPrimaryByOrg = await getBrandPrimaryDomainsForOrgs(orgIds); + const brandDomains = Array.from(brandPrimaryByOrg.values()); const brandsMap = await this.brandDb.getDiscoveredBrandsByDomains(brandDomains); for (const profile of profiles) { - if (profile.primary_brand_domain) { - const brand = brandsMap.get(profile.primary_brand_domain.toLowerCase()); + const brandPrimaryDomain = brandPrimaryByOrg.get(profile.workos_organization_id); + if (brandPrimaryDomain) { + const brand = brandsMap.get(brandPrimaryDomain.toLowerCase()); if (brand?.brand_manifest) { profile.resolved_brand = resolveBrandFromJson( - profile.primary_brand_domain, + brandPrimaryDomain, brand.brand_manifest as Record, brand.domain_verified ?? false ); @@ -8471,11 +8477,12 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< // Resolve brand data from registry if linked. Skip orphaned brands — // the manifest is preserved server-side for adoption-at-claim-time // but must not surface on the public member-profile endpoint. - if (profile.primary_brand_domain) { - const brand = await this.brandDb.getDiscoveredBrandByDomain(profile.primary_brand_domain); + const brandPrimaryDomain = await getBrandPrimaryDomain(profile.workos_organization_id); + if (brandPrimaryDomain) { + const brand = await this.brandDb.getDiscoveredBrandByDomain(brandPrimaryDomain); if (brand?.brand_manifest && !brand.manifest_orphaned) { profile.resolved_brand = resolveBrandFromJson( - profile.primary_brand_domain, + brandPrimaryDomain, brand.brand_manifest as Record, brand.domain_verified ?? false ); diff --git a/server/src/routes/brand-feeds.ts b/server/src/routes/brand-feeds.ts index 6c4f9bc4b2..9a1ba9b53d 100644 --- a/server/src/routes/brand-feeds.ts +++ b/server/src/routes/brand-feeds.ts @@ -20,6 +20,7 @@ import { VALID_PROPERTY_TYPES, type Relationship, } from '../services/brand-property-parse.js'; +import { getBrandPrimaryDomain } from '../services/brand-domain-resolver.js'; const MAX_COLLECTIONS = 200; const VALID_COLLECTION_KINDS = ['series', 'publication', 'event_series', 'rotation']; @@ -51,13 +52,15 @@ export function createBrandFeedsRouter(config: { brandDb: BrandDatabase }) { 'SELECT domain FROM organization_domains WHERE workos_organization_id = $1', [orgId] ); - const memberProfile = await query<{ primary_brand_domain: string | null }>( - 'SELECT primary_brand_domain FROM member_profiles WHERE workos_organization_id = $1', - [orgId] - ); + // Brand-primary via Stage 1 resolver (org_domains.is_primary first, + // member_profiles fallback for orgs Stage 0 missed). Post-Stage-0 the + // brand-primary is already in organization_domains, so this entry is + // usually redundant with one of the orgDomains rows above — kept + // explicit so the fallback still adds coverage during the transition. + const brandPrimary = await getBrandPrimaryDomain(orgId); const ownedDomains = new Set([ ...orgDomains.rows.map(r => r.domain.toLowerCase()), - ...(memberProfile.rows[0]?.primary_brand_domain ? [memberProfile.rows[0].primary_brand_domain.toLowerCase()] : []), + ...(brandPrimary ? [brandPrimary.toLowerCase()] : []), ]); if (!ownedDomains.has(domain.toLowerCase())) { return { error: 'You do not own this brand domain', status: 403 }; From cd513e38258982d85d6d690bd950854b89d38bfc Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 9 May 2026 14:00:56 -0400 Subject: [PATCH 2/2] 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) --- server/src/http.ts | 7 ++++--- server/src/routes/brand-feeds.ts | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/server/src/http.ts b/server/src/http.ts index 94202a58f6..70fa8f6421 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -8310,9 +8310,10 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< offset: offset ? parseInt(offset as string, 10) : 0, }); - // Batch-fetch all brand data and credentials in three queries instead of N+1. - // Brand-primary domains come from the Stage 1 resolver (org_domains.is_primary - // first, member_profiles fallback), keyed by org_id rather than a per-row column. + // Batch-fetch brand data and credentials in a constant number of queries + // (resolver + brandsMap + credentialsMap) instead of N+1. Brand-primary + // domains come from the Stage 1 resolver (org_domains.is_primary first, + // member_profiles fallback), keyed by org_id rather than a per-row column. const orgIds = profiles.map(p => p.workos_organization_id); const brandPrimaryByOrg = await getBrandPrimaryDomainsForOrgs(orgIds); const brandDomains = Array.from(brandPrimaryByOrg.values()); diff --git a/server/src/routes/brand-feeds.ts b/server/src/routes/brand-feeds.ts index 9a1ba9b53d..bc46258ed3 100644 --- a/server/src/routes/brand-feeds.ts +++ b/server/src/routes/brand-feeds.ts @@ -48,15 +48,15 @@ export function createBrandFeedsRouter(config: { brandDb: BrandDatabase }) { return { error: 'No organization associated with your account', status: 403 }; } + // TODO(#4159): the orgDomains walk doesn't filter on verified=true; same + // for the resolver's fallback. Pre-existing trust gap — an unverified + // org_domains row or stale member_profiles.primary_brand_domain grants + // brand-feed write authority to the org owner. Stage 2 should add the + // verified gate when the column drops. const orgDomains = await query<{ domain: string }>( 'SELECT domain FROM organization_domains WHERE workos_organization_id = $1', [orgId] ); - // Brand-primary via Stage 1 resolver (org_domains.is_primary first, - // member_profiles fallback for orgs Stage 0 missed). Post-Stage-0 the - // brand-primary is already in organization_domains, so this entry is - // usually redundant with one of the orgDomains rows above — kept - // explicit so the fallback still adds coverage during the transition. const brandPrimary = await getBrandPrimaryDomain(orgId); const ownedDomains = new Set([ ...orgDomains.rows.map(r => r.domain.toLowerCase()),