Spaces:
Running
Running
| // Redirect map builder (pure, testable). | |
| // | |
| // The producer's warehouse makes the GROUP the addressable model (its | |
| // `model_id` / `model_route_id`). Every variant, dated snapshot, or | |
| // older-cased spelling the producer folds into a group lives only in that group | |
| // row's `raw_model_ids` list and has no page of its own. This builds the | |
| // old-route -> canonical-group-route 301 map straight from `models_view`, so a | |
| // link to any folded spelling lands on the real group page. Every redirect | |
| // target is an addressable `model_route_id` by construction. | |
| // | |
| // Kept separate from the generated data file (`lib/model-url-redirects.ts`) so | |
| // the generator script AND unit tests can exercise the build logic without | |
| // importing the (large) generated map. | |
| // | |
| // Route id form: RFC 3986 percent-encoded whole id (`/` -> `%2F`), matching the | |
| // producer's `route_id` column and middleware's `encodeURIComponent` lookup key. | |
| export interface ModelsViewRow { | |
| /** canonical encoded route for this group — the redirect TARGET. */ | |
| model_route_id: string | |
| model_id?: string | null | |
| route_id?: string | null | |
| model_key?: string | null | |
| model_group_id?: string | null | |
| /** raw spellings folded into this group (HF-cased, may contain `/`). */ | |
| raw_model_ids?: string[] | null | |
| } | |
| /** Encode a plain model id into the producer `route_id` form. */ | |
| export function routeId(id: string): string { | |
| return encodeURIComponent(id.trim()) | |
| } | |
| export interface BuildResult { | |
| /** old encoded route id -> canonical (group) encoded route id. */ | |
| redirects: Map<string, string> | |
| /** | |
| * Raw spellings that fan out to MORE THAN ONE group (would be ambiguous to | |
| * redirect). Excluded from the map and surfaced for review rather than | |
| * picking one arbitrarily. Expected to be empty for a well-formed warehouse. | |
| */ | |
| ambiguous: Map<string, Set<string>> | |
| } | |
| /** | |
| * Build the redirect map from models_view rows. Pure and deterministic. | |
| * | |
| * A folded spelling is redirected to its owning group's `model_route_id` unless | |
| * (a) it already equals that route, or (b) it is itself an addressable page | |
| * (some row's own id) — we never hijack a working URL. A spelling that maps to | |
| * multiple distinct groups is recorded as ambiguous and excluded. | |
| */ | |
| export function buildRedirectsFromModelsView(rows: ModelsViewRow[]): BuildResult { | |
| // Pass 1: every route form that resolves to a real page (mirrors the | |
| // getModelSummaryById lookup columns), so we never redirect a live URL. | |
| const addressable = new Set<string>() | |
| for (const r of rows) { | |
| if (r.model_route_id) addressable.add(r.model_route_id) | |
| if (r.route_id) addressable.add(r.route_id) | |
| if (r.model_id) addressable.add(routeId(r.model_id)) | |
| if (r.model_group_id) addressable.add(routeId(r.model_group_id)) | |
| if (r.model_key) addressable.add(routeId(r.model_key)) | |
| } | |
| // Pass 2: map each folded raw spelling to its owning group route. | |
| const candidates = new Map<string, Set<string>>() | |
| for (const r of rows) { | |
| const target = r.model_route_id | |
| if (!target) continue | |
| for (const raw of r.raw_model_ids ?? []) { | |
| if (!raw) continue | |
| const old = routeId(raw) | |
| if (old === target) continue // already the canonical route | |
| if (addressable.has(old)) continue // has its own page; don't hijack it | |
| const set = candidates.get(old) ?? new Set<string>() | |
| set.add(target) | |
| candidates.set(old, set) | |
| } | |
| } | |
| const redirects = new Map<string, string>() | |
| const ambiguous = new Map<string, Set<string>>() | |
| for (const [old, targets] of candidates) { | |
| if (targets.size === 1) redirects.set(old, [...targets][0]) | |
| else ambiguous.set(old, targets) | |
| } | |
| return { redirects, ambiguous } | |
| } | |
| /** Serialise a redirect map to the generated TS module source. */ | |
| export function serializeRedirectModule( | |
| redirects: Map<string, string>, | |
| meta: { source: string }, | |
| ): string { | |
| const sorted = [...redirects.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) | |
| const entries = sorted.map(([o, n]) => ` [${JSON.stringify(o)}, ${JSON.stringify(n)}],`).join("\n") | |
| return `// GENERATED by scripts/generate-model-redirects.ts — do not edit by hand. | |
| // | |
| // Derived from the warehouse models_view (${meta.source}). Each entry maps a | |
| // folded model spelling (a variant / dated snapshot / older-cased id the | |
| // producer collapses into a group) to that group's canonical route, so a link | |
| // to any folded spelling 301s to the real group page. Every target is an | |
| // addressable model_route_id by construction. | |
| // | |
| // Entries: ${redirects.size}. | |
| // | |
| // Old encoded route id -> canonical encoded route id (RFC 3986, \`/\` -> %2F). | |
| // middleware.ts looks up the incoming route here and issues a 301 (query params | |
| // preserved). | |
| /** old encoded route id -> canonical (group) encoded route id. */ | |
| export const MODEL_URL_REDIRECTS = new Map<string, string>([ | |
| ${entries} | |
| ]) | |
| export function resolveModelRedirect(routeId: string): string | undefined { | |
| return MODEL_URL_REDIRECTS.get(routeId) | |
| } | |
| ` | |
| } | |