Rebuild the connector catalog with gendered Male/Female port types

Replaces the old ungendered catalog (plain XLR, TS/TRS 1/4", Speakon
NL2/4/8, etc.) with a full Male/Female-aware set covering audio, video,
network/data, USB (with Host/Device role split for USB-C), power, RF coax,
and fiber connectors, plus real-world bridging/adapter cables — enough to
model gender benders and specify exact cable types in the BOM instead of
just a connector family. The catalog now lives entirely in the DB (seeded
via supabase/seed.sql, captured with the new export-seed.mjs) rather than
partly hardcoded in domain/library.ts, which is now unused.

Existing seeded diagrams/devices were intentionally not migrated to the
new port types — there was nothing important in them yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-28 10:05:41 -05:00
co-authored by Claude Sonnet 5
parent 6cdfde822d
commit a5c2ad87e1
6 changed files with 463 additions and 433 deletions
+166
View File
@@ -0,0 +1,166 @@
// Generates supabase/seed.sql from the local Supabase database's own public
// catalog rows (is_public = true), rather than from a hand-written source
// file — supersedes the old generate-seed.mjs/src/domain/library.ts pair, so
// the app's own device/category/port/cable-type editors (used as an Admin,
// with "Publish directly to the shared library" checked) become the one way
// new seed data gets authored. Run via: npm run export-seed
//
// `--check` instead diffs what this would write against the current
// supabase/seed.sql and exits 1 without writing if they differ — used by
// `npm run db:reset` to refuse to blow away catalog entries you haven't
// exported yet (see package.json).
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
const DB_CONTAINER = 'supabase_db_av-planner'
const SEED_PATH = new URL('../supabase/seed.sql', import.meta.url)
function queryJson(sql) {
const wrapped = `select coalesce(json_agg(t), '[]'::json) from (${sql}) t;`
const output = execFileSync(
'docker',
['exec', DB_CONTAINER, 'psql', '-U', 'postgres', '-d', 'postgres', '-t', '-A', '-c', wrapped],
{ encoding: 'utf8' },
)
return JSON.parse(output.trim())
}
function sqlString(value) {
if (value === undefined || value === null) return 'null'
return `'${String(value).replace(/'/g, "''")}'`
}
function sqlTextArray(values) {
if (!values || values.length === 0) return "'{}'"
return `'{${values.map((v) => `"${v.replace(/"/g, '\\"')}"`).join(',')}}'`
}
function sqlNumber(value) {
return value === undefined || value === null ? 'null' : String(value)
}
function loadCatalog() {
return {
categories: queryJson('select id, name from device_categories where is_public order by name'),
manufacturers: queryJson('select id, name from manufacturers where is_public order by name'),
portTypes: queryJson(
'select id, name, category, family, compatible_family_ids, max_connections from port_types where is_public order by name',
),
cableTypes: queryJson(
'select id, name, family, family2, unit, cost_per_unit from cable_types where is_public order by name',
),
deviceTemplates: queryJson(
'select id, name, category_id, manufacturer_id, model, cost from device_templates where is_public order by name',
),
devicePorts: queryJson(
`select id, device_template_id, name, direction, port_type_id, sort_order, built_in_cable
from device_template_ports
where device_template_id in (select id from device_templates where is_public)
order by device_template_id, sort_order`,
),
}
}
function buildSeedContent(catalog) {
const { categories, manufacturers, portTypes, cableTypes, deviceTemplates, devicePorts } = catalog
const lines = []
lines.push("-- Generated by scripts/export-seed.mjs from the local Supabase database's public")
lines.push('-- catalog rows. Do not hand-edit — author new devices/connectors via the app\'s')
lines.push('-- GUI instead (as an Admin, check "Publish directly to the shared library"),')
lines.push('-- then re-run: npm run export-seed')
lines.push('-- Seeds the public catalog: is_public = true, owner_id = null (system-owned).')
lines.push('')
lines.push('-- Device categories')
for (const c of categories) {
lines.push(`insert into public.device_categories (id, name, is_public) values (${sqlString(c.id)}, ${sqlString(c.name)}, true);`)
}
lines.push('')
lines.push('-- Manufacturers')
for (const m of manufacturers) {
lines.push(`insert into public.manufacturers (id, name, is_public) values (${sqlString(m.id)}, ${sqlString(m.name)}, true);`)
}
lines.push('')
lines.push('-- Port types')
for (const pt of portTypes) {
lines.push(
`insert into public.port_types (id, name, category, family, compatible_family_ids, max_connections, is_public) values (${sqlString(pt.id)}, ${sqlString(pt.name)}, ${sqlString(pt.category)}, ${sqlString(pt.family)}, ${sqlTextArray(pt.compatible_family_ids)}, ${sqlNumber(pt.max_connections)}, true);`,
)
}
lines.push('')
lines.push('-- Cable types')
for (const ct of cableTypes) {
lines.push(
`insert into public.cable_types (id, name, family, family2, unit, cost_per_unit, is_public) values (${sqlString(ct.id)}, ${sqlString(ct.name)}, ${sqlString(ct.family)}, ${sqlString(ct.family2)}, ${sqlString(ct.unit)}, ${sqlNumber(ct.cost_per_unit)}, true);`,
)
}
lines.push('')
lines.push('-- Device templates + their ports')
const portsByTemplate = new Map()
for (const port of devicePorts) {
const list = portsByTemplate.get(port.device_template_id) ?? []
list.push(port)
portsByTemplate.set(port.device_template_id, list)
}
for (const dt of deviceTemplates) {
lines.push(
`insert into public.device_templates (id, name, category_id, manufacturer_id, model, cost, is_public) values (${sqlString(dt.id)}, ${sqlString(dt.name)}, ${sqlString(dt.category_id)}, ${sqlString(dt.manufacturer_id)}, ${sqlString(dt.model)}, ${sqlNumber(dt.cost)}, true);`,
)
for (const port of portsByTemplate.get(dt.id) ?? []) {
lines.push(
`insert into public.device_template_ports (id, device_template_id, name, direction, port_type_id, sort_order, built_in_cable) values (${sqlString(port.id)}, ${sqlString(port.device_template_id)}, ${sqlString(port.name)}, ${sqlString(port.direction)}, ${sqlString(port.port_type_id)}, ${port.sort_order}, ${port.built_in_cable});`,
)
}
}
lines.push('')
return lines.join('\n') + '\n'
}
function summarize(catalog) {
return `${catalog.categories.length} categories, ${catalog.manufacturers.length} manufacturers, ${catalog.portTypes.length} port types, ${catalog.cableTypes.length} cable types, ${catalog.deviceTemplates.length} device templates`
}
const isCheck = process.argv.includes('--check')
let catalog
try {
catalog = loadCatalog()
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
if (isCheck) {
// Best-effort safety net, not a hard gate — if the local DB isn't
// reachable (Docker down, container not started yet) don't block a
// reset over it.
console.warn(`Couldn't reach the local Supabase database to check seed.sql — proceeding without checking.\n${message}`)
process.exit(0)
}
console.error(`Failed to read the public catalog from the local Supabase database.\n${message}`)
process.exit(1)
}
const content = buildSeedContent(catalog)
if (isCheck) {
const current = existsSync(SEED_PATH) ? readFileSync(SEED_PATH, 'utf8') : ''
if (current === content) {
console.log('supabase/seed.sql matches the database — safe to reset.')
process.exit(0)
}
console.error(
[
"supabase/seed.sql is out of date with the local database's public catalog.",
'Resetting now would discard whatever you\'ve published since it was last exported.',
'',
'Run `npm run export-seed` first to capture it, or run `npx supabase db reset` directly if you want to discard it.',
].join('\n'),
)
process.exit(1)
}
writeFileSync(SEED_PATH, content)
console.log(`Wrote supabase/seed.sql (${summarize(catalog)})`)
-62
View File
@@ -1,62 +0,0 @@
// Generates supabase/seed.sql from src/domain/library.ts, so the public
// catalog seed data is derived directly from the source of truth instead of
// hand-transcribed (and risking drift/typos). Run via: node scripts/generate-seed.mjs
import { BUILT_IN_CABLE_TYPES, BUILT_IN_DEVICE_CATEGORIES, BUILT_IN_DEVICE_TEMPLATES, BUILT_IN_PORT_TYPES } from '../src/domain/library.ts'
import { writeFileSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
function sqlString(value) {
if (value === undefined || value === null) return 'null'
return `'${String(value).replace(/'/g, "''")}'`
}
function sqlTextArray(values) {
if (!values || values.length === 0) return "'{}'"
return `'{${values.map((v) => `"${v.replace(/"/g, '\\"')}"`).join(',')}}'`
}
const lines = []
lines.push('-- Generated by scripts/generate-seed.mjs from src/domain/library.ts. Do not hand-edit —')
lines.push('-- re-run the script after changing the built-in library instead.')
lines.push('-- Seeds the public catalog: is_public = true, owner_id = null (system-owned).')
lines.push('')
lines.push('-- Device categories')
for (const c of BUILT_IN_DEVICE_CATEGORIES) {
lines.push(
`insert into public.device_categories (id, name, is_public) values (${sqlString(c.id)}, ${sqlString(c.name)}, true);`,
)
}
lines.push('')
lines.push('-- Port types')
for (const pt of BUILT_IN_PORT_TYPES) {
lines.push(
`insert into public.port_types (id, name, category, family, compatible_family_ids, max_connections, is_public) values (${sqlString(pt.id)}, ${sqlString(pt.name)}, ${sqlString(pt.category)}, ${sqlString(pt.family)}, ${sqlTextArray(pt.compatibleFamilyIds)}, ${pt.maxConnections ?? 'null'}, true);`,
)
}
lines.push('')
lines.push('-- Cable types')
for (const ct of BUILT_IN_CABLE_TYPES) {
lines.push(
`insert into public.cable_types (id, name, family, family2, unit, is_public) values (${sqlString(ct.id)}, ${sqlString(ct.name)}, ${sqlString(ct.family)}, ${sqlString(ct.family2)}, ${sqlString(ct.unit)}, true);`,
)
}
lines.push('')
lines.push('-- Device templates + their ports')
for (const dt of BUILT_IN_DEVICE_TEMPLATES) {
lines.push(
`insert into public.device_templates (id, name, category_id, is_public) values (${sqlString(dt.id)}, ${sqlString(dt.name)}, ${sqlString(dt.category)}, true);`,
)
for (const [index, port] of dt.ports.entries()) {
lines.push(
`insert into public.device_template_ports (id, device_template_id, name, direction, port_type_id, sort_order) values (${sqlString(randomUUID())}, ${sqlString(dt.id)}, ${sqlString(port.name)}, ${sqlString(port.direction)}, ${sqlString(port.portTypeId)}, ${index});`,
)
}
}
lines.push('')
writeFileSync(new URL('../supabase/seed.sql', import.meta.url), lines.join('\n') + '\n')
console.log(`Wrote supabase/seed.sql (${lines.length} lines)`)