Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/stage1-3-dashboard-feeds-resolver.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 25 additions & 17 deletions server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -8309,11 +8310,13 @@ ${p.category ? `<category>${p.category}</category>\n` : ''}<url>${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 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());

const [brandsMap, credentialsMap] = await Promise.all([
this.brandDb.getDiscoveredBrandsByDomains(brandDomains),
Expand All @@ -8323,11 +8326,12 @@ ${p.category ? `<category>${p.category}</category>\n` : ''}<url>${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<string, unknown>,
brand.domain_verified ?? false
);
Expand All @@ -8353,19 +8357,22 @@ ${p.category ? `<category>${p.category}</category>\n` : ''}<url>${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<string, unknown>,
brand.domain_verified ?? false
);
Expand Down Expand Up @@ -8471,11 +8478,12 @@ ${p.category ? `<category>${p.category}</category>\n` : ''}<url>${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<string, unknown>,
brand.domain_verified ?? false
);
Expand Down
13 changes: 8 additions & 5 deletions server/src/routes/brand-feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -47,17 +48,19 @@ 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]
);
const memberProfile = await query<{ primary_brand_domain: string | null }>(
'SELECT primary_brand_domain FROM member_profiles WHERE workos_organization_id = $1',
[orgId]
);
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 };
Expand Down
Loading