SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%

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>
Simon-Pierre Boucher committed 11 days ago (Sep 13, 2026) parent 57a597a

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