Wire the app to the shared Supabase catalog (public catalog read integration)

Moves port/cable/category/device-template data from per-diagram embedded
storage to the global public/private catalog backed by Supabase, per
organized-ideas.md's "live reference, not snapshot" decision.

- domain/types.ts, project.ts, compatibility.ts, bom.ts: lookup functions
  now take an explicit Catalog parameter instead of deriving data from
  Project — Project is reduced to just diagram-scoped fields.
- New CatalogRepository/SupabaseCatalogRepository (mirrors the
  DiagramRepository pattern) and catalogStore.ts, replacing the
  catalog-related actions that used to live in projectStore.
- device_templates gets a plain-text manufacturer column for now (the
  normalized manufacturer catalog from organized-ideas.md §3 is its own
  future pass, not blocking this one).
- domain/library.ts is no longer imported by the app — it's now only the
  source scripts/generate-seed.mjs reads to produce supabase/seed.sql.
- Swept every UI call site via tsc -b until clean; oxlint clean; 23/23
  pgTAP RLS tests still passing after a `supabase db reset`.
This commit is contained in:
2026-09-07 23:42:00 -05:00
parent 93cb4a9617
commit b97777444a
22 changed files with 656 additions and 323 deletions
+30 -63
View File
@@ -1,58 +1,46 @@
import { v4 as uuid } from 'uuid'
import { BUILT_IN_CABLE_TYPES, BUILT_IN_DEVICE_CATEGORIES, BUILT_IN_DEVICE_TEMPLATES, BUILT_IN_PORT_TYPES } from './library'
import type { CableType, Device, DeviceCategoryDef, DeviceTemplate, Port, PortType, Project } from './types'
import type { CableType, Catalog, Device, DeviceCategoryDef, DeviceTemplate, PortType, Project } from './types'
/** Merges built-in + project-custom entries. Custom entries with a matching
* id win, so a project could in principle override a built-in (not exposed
* in the UI today, but keeps the merge rule simple and predictable). */
function mergeById<T extends { id: string }>(builtIns: T[], custom: T[]): T[] {
const byId = new Map(builtIns.map((item) => [item.id, item]))
for (const item of custom) byId.set(item.id, item)
return [...byId.values()]
export function allPortTypes(catalog: Catalog): PortType[] {
return catalog.portTypes
}
export function allPortTypes(project: Project): PortType[] {
return mergeById(BUILT_IN_PORT_TYPES, project.customPortTypes)
export function allCableTypes(catalog: Catalog): CableType[] {
return catalog.cableTypes
}
export function allCableTypes(project: Project): CableType[] {
return mergeById(BUILT_IN_CABLE_TYPES, project.customCableTypes)
/** `hiddenPublicIds` is a per-user UI preference (see state/catalogStore.ts),
* not part of the catalog data itself — you can hide any public template
* from your own palette without affecting anyone else or the shared entry. */
export function allDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet<string> = new Set()): DeviceTemplate[] {
return catalog.deviceTemplates.filter((t) => t.custom || !hiddenPublicIds.has(t.id))
}
export function allDeviceTemplates(project: Project): DeviceTemplate[] {
const merged = mergeById(BUILT_IN_DEVICE_TEMPLATES, project.customDeviceTemplates)
const hidden = new Set(project.hiddenBuiltInDeviceTemplateIds)
return merged.filter((t) => !(t.builtIn && hidden.has(t.id)))
/** Public templates the user has hidden, for a "restore" list. */
export function hiddenDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet<string>): DeviceTemplate[] {
return catalog.deviceTemplates.filter((t) => !t.custom && hiddenPublicIds.has(t.id))
}
/** Built-in templates the user has hidden, for a "restore" list — deleting a
* custom template is permanent, but hiding a built-in one isn't, since the
* built-in still exists in code and can always be shown again. */
export function hiddenBuiltInDeviceTemplates(project: Project): DeviceTemplate[] {
const hidden = new Set(project.hiddenBuiltInDeviceTemplateIds)
return BUILT_IN_DEVICE_TEMPLATES.filter((t) => hidden.has(t.id))
export function allDeviceCategories(catalog: Catalog): DeviceCategoryDef[] {
return catalog.deviceCategories
}
export function allDeviceCategories(project: Project): DeviceCategoryDef[] {
return mergeById(BUILT_IN_DEVICE_CATEGORIES, project.customDeviceCategories)
}
export function getDeviceCategory(project: Project, categoryId: string): DeviceCategoryDef | undefined {
return allDeviceCategories(project).find((c) => c.id === categoryId)
export function getDeviceCategory(catalog: Catalog, categoryId: string): DeviceCategoryDef | undefined {
return allDeviceCategories(catalog).find((c) => c.id === categoryId)
}
/** Friendly label for a category id, falling back to the raw id if it's
* somehow unknown (e.g. data from an older export) rather than crashing. */
export function deviceCategoryName(project: Project, categoryId: string): string {
return getDeviceCategory(project, categoryId)?.name ?? categoryId
export function deviceCategoryName(catalog: Catalog, categoryId: string): string {
return getDeviceCategory(catalog, categoryId)?.name ?? categoryId
}
export function getPortType(project: Project, portTypeId: string): PortType | undefined {
return allPortTypes(project).find((pt) => pt.id === portTypeId)
export function getPortType(catalog: Catalog, portTypeId: string): PortType | undefined {
return allPortTypes(catalog).find((pt) => pt.id === portTypeId)
}
export function getCableType(project: Project, cableTypeId: string): CableType | undefined {
return allCableTypes(project).find((ct) => ct.id === cableTypeId)
export function getCableType(catalog: Catalog, cableTypeId: string): CableType | undefined {
return allCableTypes(catalog).find((ct) => ct.id === cableTypeId)
}
/** The set of connector families a port type will mate with: its own family
@@ -82,9 +70,9 @@ function cableFits(cableType: CableType, sourceFamilies: Set<string>, targetFami
/** Cable types usable with the given port type at (at least) one end — for
* a plain cable that means its family matches; for an adapter cable it's
* enough that either end matches. */
export function cableTypesForPortType(project: Project, portType: PortType): CableType[] {
export function cableTypesForPortType(catalog: Catalog, portType: PortType): CableType[] {
const families = acceptedFamilies(portType)
return allCableTypes(project).filter((ct) => {
return allCableTypes(catalog).filter((ct) => {
const [endA, endB] = cableEnds(ct)
return families.has(endA) || families.has(endB)
})
@@ -102,21 +90,17 @@ export function cableTypesForPortType(project: Project, portType: PortType): Cab
* aren't declared generally compatible still be wired with the right cable,
* without changing either port type's own compatibility rules.
*/
export function cableTypesForConnection(
project: Project,
sourcePortType: PortType,
targetPortType: PortType,
): CableType[] {
export function cableTypesForConnection(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): CableType[] {
const sourceFamilies = acceptedFamilies(sourcePortType)
const targetFamilies = acceptedFamilies(targetPortType)
return allCableTypes(project).filter((ct) => cableFits(ct, sourceFamilies, targetFamilies))
return allCableTypes(catalog).filter((ct) => cableFits(ct, sourceFamilies, targetFamilies))
}
/** Whether any cable type (built-in or custom) could bridge these two port
* types — used to allow a connection even when the port types aren't
* declared directly compatible, as long as a specific adapter cable exists. */
export function hasBridgingCableType(project: Project, sourcePortType: PortType, targetPortType: PortType): boolean {
return cableTypesForConnection(project, sourcePortType, targetPortType).length > 0
export function hasBridgingCableType(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): boolean {
return cableTypesForConnection(catalog, sourcePortType, targetPortType).length > 0
}
/**
@@ -150,20 +134,12 @@ export function createEmptyProject(name = 'Untitled Project'): Project {
updatedAt: now,
devices: [],
connections: [],
customDeviceTemplates: [],
customPortTypes: [],
customCableTypes: [],
customDeviceCategories: [],
hiddenBuiltInDeviceTemplateIds: [],
}
}
/** Instantiates a device on the canvas from a template, deep-copying ports
* (with fresh ids) so editing the instance never mutates the template. */
export function createDeviceFromTemplate(
template: DeviceTemplate,
position: { x: number; y: number },
): Device {
export function createDeviceFromTemplate(template: DeviceTemplate, position: { x: number; y: number }): Device {
return {
id: uuid(),
name: template.name,
@@ -177,15 +153,6 @@ export function createDeviceFromTemplate(
}
}
export function createBlankPort(overrides: Partial<Port> = {}): Port {
return {
id: uuid(),
name: overrides.name ?? 'New Port',
direction: overrides.direction ?? 'output',
portTypeId: overrides.portTypeId ?? BUILT_IN_PORT_TYPES[0].id,
}
}
/** "CV Out 1" -> "CV Out 2", "Port 3" -> "Port 4", "Gate In" -> "Gate In 2".
* Only increments a number that's the very last thing in the name (trailing
* whitespace aside), so labels like `1/4" TS In` aren't misread as ending