From 4e70f281ae3a9aa2bc8d0bfd4a346e7058398183 Mon Sep 17 00:00:00 2001 From: Chad Miller Date: Thu, 27 Aug 2026 09:00:58 -0700 Subject: [PATCH] fix: restore location on gallery detail and unbreak one-word place names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate bugs, both reached from the same link. getGallery hand-assembled its record from SELECT * and never read the `address` column, so hydrateGalleries could not build `locationDisplay` and a gallery detail page showed no location at all — while the same gallery in the feed showed "Waldport, Oregon, US". It now uses getRecords like every other caller, which reconstructs the whole record; that is why getActorFavorites and searchGalleries never had this bug, and it means a field added to Gallery later cannot be silently dropped again. 43 lines become 4. expandCountryAliases threw instead of returning nothing for a string that is not a country. normalizeCountry passes unrecognised input straight through, so `canon` is routinely something like "PARIS", and Intl.DisplayNames.of() raises RangeError on anything that is not a valid region code. The location feed tries a single-word display name as a country first, so every one-word place — Paris, Portland, Waldport, Forest Park — failed the whole request with invalid_argument before any matching ran, including the sibling locality interpretation that would have matched. The Intl call is now guarded. The feed's interpretation loop also staged its bind values so an unsatisfiable interpretation cannot leave orphaned parameters behind and shift every later placeholder number. Verified against local data: Paris, Portland and Forest Park return results where they previously errored, and the multi-part forms are unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XPZzUb2PBrkuHzcAMfGLLY --- server/feeds/location.ts | 43 +++++++++++++++++++++++++++++---------- server/helpers/country.ts | 8 ++++++-- server/xrpc/getGallery.ts | 43 ++++----------------------------------- 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/server/feeds/location.ts b/server/feeds/location.ts index 40e025c..fc95901 100644 --- a/server/feeds/location.ts +++ b/server/feeds/location.ts @@ -101,29 +101,50 @@ export default defineFeed({ const interpClauses: string[] = []; for (const interp of interps) { + // Staged locally: an interpretation that turns out to be unsatisfiable + // must not leave its bind values behind, or every later placeholder + // number would refer to the wrong parameter. const matches: string[] = []; + const staged: any[] = []; + let next = p; + let usable = true; + if (interp.locality) { - matches.push(`UPPER(json_extract(t.address, '$.locality')) = UPPER($${p++})`); - params.push(interp.locality); + matches.push(`UPPER(json_extract(t.address, '$.locality')) = UPPER($${next++})`); + staged.push(interp.locality); } if (interp.region) { - matches.push(`UPPER(json_extract(t.address, '$.region')) = UPPER($${p++})`); - params.push(interp.region); + matches.push(`UPPER(json_extract(t.address, '$.region')) = UPPER($${next++})`); + staged.push(interp.region); } if (interp.country) { - // Expand "US"/"USA"/etc. all together. + // Expand "US"/"USA"/etc. all together. A single-word display name is + // tried as a country first ("Greece"), so this is routinely handed + // something that is not one ("Waldport") and gets back no aliases. + // Emitting `IN ()` is a SQLite syntax error that fails the entire + // query — including the sibling locality interpretation that would + // have matched — so drop this interpretation instead. const aliases = expandCountryAliases(interp.country); - const placeholders = aliases.map(() => `$${p++}`).join(","); - matches.push(`UPPER(json_extract(t.address, '$.country')) IN (${placeholders})`); - params.push(...aliases); + if (aliases.length === 0) { + usable = false; + } else { + const placeholders = aliases.map(() => `$${next++}`).join(","); + matches.push(`UPPER(json_extract(t.address, '$.country')) IN (${placeholders})`); + staged.push(...aliases); + } } if (interp.regionNullOrEqual) { matches.push( - `(json_extract(t.address, '$.region') IS NULL OR UPPER(json_extract(t.address, '$.region')) = UPPER($${p++}))`, + `(json_extract(t.address, '$.region') IS NULL OR UPPER(json_extract(t.address, '$.region')) = UPPER($${next++}))`, ); - params.push(interp.regionNullOrEqual); + staged.push(interp.regionNullOrEqual); + } + + if (usable && matches.length) { + interpClauses.push(`(${matches.join(" AND ")})`); + params.push(...staged); + p = next; } - if (matches.length) interpClauses.push(`(${matches.join(" AND ")})`); } // Also match records whose raw location.name equals the requested name — diff --git a/server/helpers/country.ts b/server/helpers/country.ts index a712faa..8f4523d 100644 --- a/server/helpers/country.ts +++ b/server/helpers/country.ts @@ -73,7 +73,11 @@ export function expandCountryAliases(raw: string): string[] { for (const [alias, c] of Object.entries(COUNTRY_ALIASES)) { if (c === canon) set.add(alias); } - const fullName = regionNames?.of(canon); - if (fullName) set.add(fullName.toUpperCase()); + try { + const fullName = regionNames?.of(canon); + if (fullName) set.add(fullName.toUpperCase()); + } catch { + // Not a region code — the canonical string alone is all we can offer. + } return [...set]; } diff --git a/server/xrpc/getGallery.ts b/server/xrpc/getGallery.ts index 6e2de3f..c9e41a1 100644 --- a/server/xrpc/getGallery.ts +++ b/server/xrpc/getGallery.ts @@ -4,51 +4,16 @@ import { hydrateGalleries } from "../hydrate/galleries.ts"; import { resolveAtUri } from "../helpers/resolveHandle.ts"; export default defineQuery("social.grain.unspecced.getGallery", async (ctx) => { - const { ok, params, db } = ctx; + const { ok, params, db, getRecords } = ctx; let { gallery: galleryUri } = params; // Resolve handle in AT URI if needed const resolved = await resolveAtUri(db, galleryUri); if (resolved) galleryUri = resolved; - const rows = (await db.query(`SELECT * FROM "social.grain.gallery" WHERE uri = $1`, [ - galleryUri, - ])) as Array<{ - uri: string; - did: string; - cid: string; - handle: string | null; - indexed_at: string | null; - title: string; - description: string | null; - facets: string | null; - labels: string | null; - location: string | null; - updated_at: string | null; - created_at: string; - }>; - - const row = rows[0]; - if (!row) throw new InvalidRequestError("Gallery not found"); - - const record = { - title: row.title, - description: row.description ?? undefined, - facets: row.facets ? JSON.parse(row.facets) : undefined, - labels: row.labels ? JSON.parse(row.labels) : undefined, - location: row.location ? JSON.parse(row.location) : undefined, - updatedAt: row.updated_at ?? undefined, - createdAt: row.created_at, - }; - - const galleryRow = { - uri: row.uri, - did: row.did, - cid: row.cid, - handle: row.handle ?? undefined, - indexed_at: row.indexed_at ?? undefined, - value: record as Gallery, - }; + const recordsMap = await getRecords("social.grain.gallery", [galleryUri]); + const galleryRow = recordsMap.get(galleryUri); + if (!galleryRow) throw new InvalidRequestError("Gallery not found"); const [galleryView] = await hydrateGalleries(ctx, [galleryRow]); -- 2.51.2