factory: build text[] parameters with textArray() — apostrophes in aliases (Moody's) broke the seed upsert
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 changed files +4 −4
modified
apps/api/src/routes-admin.ts
+2 −2
@@ -5,7 +5,7 @@ import YAML from "yaml"; | ||
| 5 | 5 | import { z } from "zod"; |
| 6 | 6 | import { extendSchema, importDocumentSchema, sourceSchema, type SensorEndpoint, type Tier } from "@websensor/core"; |
| 7 | 7 | import { getConnector, listConnectors, NormalizeError } from "@websensor/connectors"; |
| 8 | −import { db, sql } from "@websensor/db"; | |
| 8 | +import { db, sql, textArray } from "@websensor/db"; | |
| 9 | 9 | import { cacheStats, invalidate } from "./cache"; |
| 10 | 10 | import { config } from "./config"; |
| 11 | 11 | import { engineStatus, factoryStatus, liveStats } from "./live"; |
@@ -221,7 +221,7 @@ export async function registerAdminRoutes(app: FastifyInstance): Promise<void> { | ||
| 221 | 221 | const domain = s.domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, ""); |
| 222 | 222 | const id = s.id ?? domain.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60); |
| 223 | 223 | await db.execute(sql`insert into factory_seeds (id, name, domain, categories, country, language, tier, importance, aliases, first_party, sector, universe, hints, status) |
| 224 | − values (${id}, ${s.name}, ${domain}, ${sql.raw("'{" + s.categories.map((c) => '"' + c.replace(/"/g, "") + '"').join(",") + "}'::text[]")}, ${s.country ?? null}, ${s.language ?? null}, ${s.tier}, ${s.importance}, ${sql.raw("'{" + s.aliases.map((c) => '"' + c.replace(/["\\]/g, "") + '"').join(",") + "}'::text[]")}, ${s.first_party}, ${s.sector ?? null}, ${s.universe ?? null}, ${JSON.stringify(s.hints)}::jsonb, 'queued') | |
| 224 | + values (${id}, ${s.name}, ${domain}, ${textArray(s.categories)}, ${s.country ?? null}, ${s.language ?? null}, ${s.tier}, ${s.importance}, ${textArray(s.aliases)}, ${s.first_party}, ${s.sector ?? null}, ${s.universe ?? null}, ${JSON.stringify(s.hints)}::jsonb, 'queued') | |
| 225 | 225 | on conflict (id) do update set name = excluded.name, hints = factory_seeds.hints || excluded.hints, status = 'queued', updated_at = now()`); |
| 226 | 226 | n++; |
| 227 | 227 | } |
modified
apps/engine/src/factory/seeds.ts
+2 −2
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; | ||
| 2 | 2 | import { join } from "node:path"; |
| 3 | 3 | import YAML from "yaml"; |
| 4 | 4 | import { coverageKey, coverageSectorSchema, slugify, type CoverageMember, type CoverageSector } from "@websensor/core"; |
| 5 | −import { db, factorySeeds, sql } from "@websensor/db"; | |
| 5 | +import { db, factorySeeds, sql, textArray } from "@websensor/db"; | |
| 6 | 6 | import { factoryConfig, log } from "../config"; |
| 7 | 7 | |
| 8 | 8 | /** |
@@ -109,7 +109,7 @@ export async function upsertSeeds(inputs: SeedInput[], opts: { requeue?: boolean | ||
| 109 | 109 | } |
| 110 | 110 | const r = await db.execute<{ inserted: boolean }>(sql` |
| 111 | 111 | insert into factory_seeds (id, name, domain, homepage, categories, country, language, tier, weight, importance, aliases, first_party, sector, universe, hints, status) |
| 112 | − values (${id}, ${s.name}, ${domain}, ${s.homepage ?? null}, ${sql.raw("'{" + (s.categories ?? []).map((c) => '"' + c.replace(/"/g, "") + '"').join(",") + "}'::text[]")}, ${s.country ?? null}, ${s.language ?? null}, ${s.tier ?? "B"}, ${s.weight ?? 1}, ${s.importance ?? 2}, ${sql.raw("'{" + (s.aliases ?? []).map((c) => '"' + c.replace(/["\\]/g, "") + '"').join(",") + "}'::text[]")}, ${s.first_party ?? true}, ${s.sector ?? null}, ${s.universe ?? null}, ${JSON.stringify(s.hints ?? {})}::jsonb, 'queued') | |
| 112 | + values (${id}, ${s.name}, ${domain}, ${s.homepage ?? null}, ${textArray(s.categories ?? [])}, ${s.country ?? null}, ${s.language ?? null}, ${s.tier ?? "B"}, ${s.weight ?? 1}, ${s.importance ?? 2}, ${textArray(s.aliases ?? [])}, ${s.first_party ?? true}, ${s.sector ?? null}, ${s.universe ?? null}, ${JSON.stringify(s.hints ?? {})}::jsonb, 'queued') | |
| 113 | 113 | on conflict (id) do update set name = excluded.name, domain = excluded.domain, categories = excluded.categories, country = coalesce(excluded.country, factory_seeds.country), language = coalesce(excluded.language, factory_seeds.language), tier = excluded.tier, importance = excluded.importance, aliases = excluded.aliases, sector = coalesce(excluded.sector, factory_seeds.sector), universe = coalesce(excluded.universe, factory_seeds.universe), hints = factory_seeds.hints || excluded.hints, updated_at = now()${opts.requeue ? sql`, status = 'queued'` : sql``} |
| 114 | 114 | returning (xmax = 0) as inserted`); |
| 115 | 115 | if (r.rows[0]?.inserted) inserted++; |
| 116 | 116 | |