import type { MetabaseClient } from './client.js' import type { MetabaseCollection, MetabaseDatabase } from './types.js' /** * Resolves human-readable names to Metabase numeric IDs. * Caches database metadata, collections, and cards for efficient lookups. */ export class Resolver { private databases = new Map() private databaseEngines = new Map() private tables = new Map() // "db.table" -> id private fields = new Map() // "db.table.field" -> id private collections = new Map() // "name" or "parent/name" -> id private collectionPaths = new Map() // id -> full path private cards = new Map() // "collection/name" -> id async init(client: MetabaseClient) { // Load databases + metadata const dbs = await client.getDatabases() for (const db of dbs) { this.databases.set(db.name, db.id) this.databaseEngines.set(db.name, db.engine) try { const meta = await client.getDatabaseMetadata(db.id) if (meta.tables) { for (const table of meta.tables) { this.tables.set(`${db.name}.${table.name}`, table.id) if (table.fields) { for (const field of table.fields) { this.fields.set(`${db.name}.${table.name}.${field.name}`, field.id) } } } } } catch { // Some databases may not expose metadata } } // Load collections const collections = await client.getCollections() this.buildCollectionMap(collections) // Load cards const cards = await client.getCards() for (const card of cards) { if (!card.archived) { const collPath = card.collection_id ? (this.collectionPaths.get(card.collection_id) ?? '') : '' const key = collPath ? `${collPath}/${card.name}` : card.name this.cards.set(key, card.id) // Also index by name only for simple lookups this.cards.set(card.name, card.id) } } } private buildCollectionMap(collections: MetabaseCollection[]) { const byId = new Map() for (const c of collections) { if (!c.archived) { byId.set(c.id, c) } } const getPath = (c: MetabaseCollection): string => { if (!c.parent_id || !byId.has(c.parent_id)) return c.name return `${getPath(byId.get(c.parent_id)!)}/${c.name}` } for (const c of collections) { if (c.archived) continue const path = getPath(c) this.collections.set(c.name, c.id) this.collections.set(path, c.id) this.collectionPaths.set(c.id, path) } } resolveDatabase(name: string): number { const id = this.databases.get(name) if (id === undefined) { const available = [...this.databases.keys()].join(', ') throw new Error(`Database "${name}" not found. Available: ${available}`) } return id } getDatabaseEngine(name: string): string { return this.databaseEngines.get(name) ?? 'unknown' } resolveCollection(name: string): number | null { if (!name || name === 'root') return null const id = this.collections.get(name) if (id === undefined) { const available = [...new Set(this.collections.values())].length throw new Error(`Collection "${name}" not found. ${available} collections available.`) } return id } resolveTable(db: string, table: string): number { const id = this.tables.get(`${db}.${table}`) if (id === undefined) throw new Error(`Table "${db}.${table}" not found`) return id } resolveField(db: string, table: string, field: string): number { const id = this.fields.get(`${db}.${table}.${field}`) if (id === undefined) throw new Error(`Field "${db}.${table}.${field}" not found`) return id } resolveCard(name: string, collection?: string): number { const key = collection ? `${collection}/${name}` : name const id = this.cards.get(key) ?? this.cards.get(name) if (id === undefined) throw new Error(`Card/Question "${key}" not found`) return id } getCollectionPath(id: number): string | undefined { return this.collectionPaths.get(id) } getDatabaseName(id: number): string | undefined { for (const [name, dbId] of this.databases) { if (dbId === id) return name } return undefined } getCollectionName(id: number | null): string { if (!id) return 'root' return this.collectionPaths.get(id) ?? `unknown-${id}` } // Register a newly created resource so subsequent resolves work registerCollection(name: string, id: number) { this.collections.set(name, id) this.collectionPaths.set(id, name) } registerCard(name: string, collection: string, id: number) { this.cards.set(name, id) if (collection) { this.cards.set(`${collection}/${name}`, id) } } }