import type { MetabaseClient } from './client.js' import type { Resolver } from './resolver.js' import type { Resource, CollectionResource, QuestionResource, DashboardResource, DashboardCardRef, DashboardTextCard, DashboardVirtualCard, SnippetResource, StateFile, } from './types.js' import { findInState, upsertState } from './state.js' export interface Action { type: 'create' | 'update' resource: Resource existingId?: number } export function buildPlan(resources: Resource[], state: StateFile): Action[] { const actions: Action[] = [] // Sort: collections first, then questions, then dashboards const sorted = [...resources].sort((a, b) => { const order: Record = { collection: 0, snippet: 1, question: 2, dashboard: 3 } return order[a.kind] - order[b.kind] }) for (const resource of sorted) { const existing = findInState( state, resource.kind, resource.name, 'collection' in resource ? (resource as any).collection : undefined, ) if (existing) { actions.push({ type: 'update', resource, existingId: existing.metabaseId }) } else { actions.push({ type: 'create', resource }) } } return actions } export async function applyActions( client: MetabaseClient, resolver: Resolver, actions: Action[], state: StateFile, ): Promise { for (const action of actions) { switch (action.resource.kind) { case 'collection': state = await applyCollection(client, resolver, action, state) break case 'question': state = await applyQuestion(client, resolver, action, state) break case 'dashboard': state = await applyDashboard(client, resolver, action, state) break case 'snippet': state = await applySnippet(client, action, state) break } } return state } async function applyCollection( client: MetabaseClient, resolver: Resolver, action: Action, state: StateFile, ): Promise { const res = action.resource as CollectionResource const parentId = res.parent ? resolver.resolveCollection(res.parent) : null if (action.type === 'create') { const created = await client.createCollection({ name: res.name, description: res.description, parent_id: parentId, }) resolver.registerCollection(res.name, created.id) return upsertState(state, { kind: 'collection', name: res.name, metabaseId: created.id }) } else { await client.updateCollection(action.existingId!, { name: res.name, description: res.description, parent_id: parentId, }) return upsertState(state, { kind: 'collection', name: res.name, metabaseId: action.existingId! }) } } async function applyQuestion( client: MetabaseClient, resolver: Resolver, action: Action, state: StateFile, ): Promise { const res = action.resource as QuestionResource const collectionId = resolver.resolveCollection(res.collection) const databaseId = resolver.resolveDatabase(res.database) let datasetQuery: Record = { type: 'query', database: databaseId } if (res.type === 'native') { datasetQuery = { type: 'native', database: databaseId, native: { query: res.query, ...(res.templateTags ? { 'template-tags': res.templateTags } : {}), }, } } else if (res.datasetQuery) { // Full dataset_query (MLv2/stages-based) — pass through as-is datasetQuery = { ...res.datasetQuery, database: databaseId } } else if (res.query) { // Structured query — resolve table/field references const mbql = res.query const [dbName, tableName] = mbql['source-table'].split('.') const tableId = resolver.resolveTable(dbName || res.database, tableName || mbql['source-table']) datasetQuery = { type: 'query', database: databaseId, query: { ...mbql, 'source-table': tableId, }, } } const cardPayload: Record = { name: res.name, description: res.description ?? null, dataset_query: datasetQuery, display: res.display ?? 'table', visualization_settings: res.visualizationSettings ?? {}, collection_id: collectionId, type: res.cardType ?? 'question', } if (action.type === 'create') { const created = await client.createCard(cardPayload) resolver.registerCard(res.name, res.collection, created.id) return upsertState(state, { kind: 'question', name: res.name, metabaseId: created.id, collection: res.collection, }) } else { await client.updateCard(action.existingId!, cardPayload) return upsertState(state, { kind: 'question', name: res.name, metabaseId: action.existingId!, collection: res.collection, }) } } async function applyDashboard( client: MetabaseClient, resolver: Resolver, action: Action, state: StateFile, ): Promise { const res = action.resource as DashboardResource const collectionId = resolver.resolveCollection(res.collection) const dashPayload: Record = { name: res.name, description: res.description ?? null, collection_id: collectionId, parameters: res.parameters ?? [], } let dashId: number if (action.type === 'create') { const created = await client.createDashboard(dashPayload) dashId = created.id } else { dashId = action.existingId! await client.updateDashboard(dashId, dashPayload) } // Add cards to dashboard if (res.cards.length > 0) { const dashcards: Record[] = [] for (const card of res.cards) { if ('question' in card) { const ref = card as DashboardCardRef const cardId = resolver.resolveCard(ref.question, res.collection) dashcards.push({ card_id: cardId, row: ref.row, col: ref.col, size_x: ref.width, size_y: ref.height, visualization_settings: ref.visualizationSettings ?? {}, parameter_mappings: ref.parameterMappings ?? [], }) } else if ('text' in card) { const text = card as DashboardTextCard dashcards.push({ card_id: null, row: text.row, col: text.col, size_x: text.width, size_y: text.height, visualization_settings: { virtual_card: { name: null, display: 'text', dataset_query: {}, visualization_settings: {}, }, text: text.text, }, parameter_mappings: text.parameterMappings ?? [], }) } else if ('virtualCard' in card) { const vc = card as DashboardVirtualCard dashcards.push({ card_id: null, row: vc.row, col: vc.col, size_x: vc.width, size_y: vc.height, visualization_settings: vc.visualizationSettings ?? { virtual_card: vc.virtualCard }, parameter_mappings: vc.parameterMappings ?? [], }) } } // Add each card individually for (const dc of dashcards) { await client.addCardToDashboard(dashId, dc) } } state = upsertState(state, { kind: 'dashboard', name: res.name, metabaseId: dashId, collection: res.collection, }) return state } async function applySnippet( client: MetabaseClient, action: Action, state: StateFile, ): Promise { const res = action.resource as SnippetResource const payload: Record = { name: res.name, description: res.description ?? null, content: res.content, } if (action.type === 'create') { const created = await client.createSnippet(payload) return upsertState(state, { kind: 'snippet', name: res.name, metabaseId: created.id }) } else { await client.updateSnippet(action.existingId!, payload) return upsertState(state, { kind: 'snippet', name: res.name, metabaseId: action.existingId! }) } }