@@ -5,7 +5,7 @@ import { getTablesWithFields } from '../getDatabaseSchema';
55import { getAllRelevantModelsForSelectedDb , getDatabaseInfo , getFieldResolvedName } from '../metabaseAPIHelpers' ;
66import { getDashboardState , getSelectedDbId } from '../metabaseStateAPI' ;
77import { getParsedIframeInfo , RPCs } from 'web' ;
8- import { getSQLFromMBQL } from '../metabaseAPI' ;
8+ import { getSQLFromMBQL , fetchCard } from '../metabaseAPI' ;
99import { metabaseToMarkdownTable } from '../operations' ;
1010import { find , get } from 'lodash' ;
1111import { getTablesFromSqlRegex , TableAndSchema } from '../parseSql' ;
@@ -19,6 +19,126 @@ import { getLimitedEntitiesFromQueries, getLimitedEntitiesFromMBQLQueries } from
1919
2020// Removed: const { getMetabaseState } = RPCs - using centralized state functions instead
2121
22+ // Helper function to generate clean slugs from card names
23+ function generateCardSlug ( cardName : string , cardId : number ) : string {
24+ if ( ! cardName ) return `card-${ cardId } ` ;
25+
26+ // Convert to lowercase, replace spaces and special characters with hyphens
27+ const slug = cardName
28+ . toLowerCase ( )
29+ . replace ( / [ ^ \w \s - ] / g, '' ) // Remove special characters except hyphens and spaces
30+ . replace ( / \s + / g, '-' ) // Replace spaces with hyphens
31+ . replace ( / - + / g, '-' ) // Replace multiple hyphens with single hyphen
32+ . replace ( / ^ - | - $ / g, '' ) ; // Remove leading/trailing hyphens
33+
34+ return `${ cardId } -${ slug } ` ;
35+ }
36+
37+ // Helper function to generate UUID-like strings for template tags
38+ function generateUUID ( ) : string {
39+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( / [ x y ] / g, function ( c ) {
40+ const r = Math . random ( ) * 16 | 0 ;
41+ const v = c === 'x' ? r : ( r & 0x3 | 0x8 ) ;
42+ return v . toString ( 16 ) ;
43+ } ) ;
44+ }
45+
46+ // Helper function to create template tag object
47+ function createTemplateTag ( cardId : number , cardName : string , cardType ?: string ) : any {
48+ const slug = generateCardSlug ( cardName , cardId ) ;
49+ const templateTagName = `#${ slug } ` ;
50+ const entityType = cardType === 'model' ? 'Model' : 'Card' ;
51+ const displayName = `#${ cardId } ${ cardName || `${ entityType } ${ cardId } ` } ` ;
52+
53+ return {
54+ type : "card" , // Metabase uses 'card' type for both cards and models in template tags
55+ name : templateTagName ,
56+ id : generateUUID ( ) ,
57+ "display-name" : displayName ,
58+ "card-id" : cardId
59+ } ;
60+ }
61+
62+ // Helper function to process MBQL cards and convert them to native SQL with template tags
63+ async function processMBQLCard (
64+ card : SavedCard ,
65+ dbId : number ,
66+ cardDetailsMap : Map < number , any >
67+ ) : Promise < SavedCard > {
68+ // Only process MBQL cards
69+ if ( card . dataset_query ?. type !== 'query' ) {
70+ return card ;
71+ }
72+
73+ try {
74+ // Get source table IDs from the MBQL query
75+ const sourceTableIds = getSourceTableIdsFromObject ( card . dataset_query . query ) ;
76+
77+ // Get SQL for the main query and child queries
78+ const [ mainSQLRaw , childSQLsRaw ] = await Promise . all ( [
79+ getSQLFromMBQL ( {
80+ database : dbId ,
81+ type : 'query' ,
82+ query : card . dataset_query . query ,
83+ } ) ,
84+ Promise . all ( sourceTableIds . map ( id => getSQLFromMBQL ( {
85+ database : dbId ,
86+ type : 'query' ,
87+ query : {
88+ 'source-table' : id
89+ } ,
90+ } ) ) )
91+ ] ) ;
92+
93+ // Process SQL strings
94+ const mainSQL = splitAndTrimSQL ( mainSQLRaw . query ) ;
95+ const childSQLs = childSQLsRaw . map ( i => splitAndTrimSQL ( i . query ) ) . map ( getOutermostParenthesesContent ) ;
96+
97+ // Create template tags map
98+ const templateTags : Record < string , any > = { } ;
99+
100+ // Replace child SQL queries with template tag references
101+ const mainSQLWithCards = childSQLs . reduce ( ( sql , childSql , index ) => {
102+ let cardId = sourceTableIds [ index ] ;
103+ if ( typeof cardId === 'string' && cardId . startsWith ( 'card__' ) ) {
104+ cardId = parseInt ( cardId . slice ( 6 ) ) ;
105+ }
106+
107+ // Get the referenced card details from the fetched card info
108+ const referencedCard = cardDetailsMap . get ( cardId ) ;
109+ const cardName = referencedCard ?. name || `Card ${ cardId } ` ;
110+ const cardType = referencedCard ?. type ;
111+
112+ // Create template tag
113+ const templateTag = createTemplateTag ( cardId , cardName , cardType ) ;
114+ templateTags [ templateTag . name ] = templateTag ;
115+
116+ // Replace SQL with template tag reference
117+ if ( sql . includes ( childSql ) ) {
118+ return sql . replace ( childSql , `{{${ templateTag . name } }}` ) ;
119+ }
120+ return sql ;
121+ } , mainSQL ) ;
122+
123+ // Create the processed card with native SQL
124+ const processedCard : SavedCard = {
125+ ...card ,
126+ dataset_query : {
127+ ...card . dataset_query ,
128+ native : {
129+ query : mainSQLWithCards ,
130+ 'template-tags' : templateTags
131+ }
132+ }
133+ } ;
134+
135+ return processedCard ;
136+ } catch ( error ) {
137+ console . error ( `Error processing MBQL card ${ card . id } :` , error ) ;
138+ return card ;
139+ }
140+ }
141+
22142function getSelectedTabDashcardIds ( dashboardMetabaseState : DashboardMetabaseState ) {
23143 const currentDashboardData = dashboardMetabaseState . dashboards ?. [ dashboardMetabaseState . dashboardId ] ;
24144 if ( ! currentDashboardData ) {
@@ -262,12 +382,103 @@ export async function getDashboardAppState(): Promise<MetabaseAppStateDashboard
262382 metabaseUrl : fullUrl ,
263383 isEmbedded : getParsedIframeInfo ( ) . isEmbedded ,
264384 } ;
385+ // Process all MBQL cards and convert them to native SQL with template tags
386+ if ( dbId ) {
387+ try {
388+ // Collect all card IDs from MBQL queries
389+ const allCardIds = new Set < number > ( ) ;
390+ for ( const card of filteredCards ) {
391+ if ( card . dataset_query ?. type === 'query' ) {
392+ const sourceTableIds = getSourceTableIdsFromObject ( card . dataset_query . query ) ;
393+ sourceTableIds . forEach ( id => {
394+ // Convert string card IDs (like "card__123") to numbers
395+ if ( typeof id === 'string' && id . startsWith ( 'card__' ) ) {
396+ allCardIds . add ( parseInt ( id . slice ( 6 ) ) ) ;
397+ } else if ( typeof id === 'number' && id > 0 ) {
398+ // Assuming card IDs are positive numbers (tables are usually negative)
399+ allCardIds . add ( id ) ;
400+ }
401+ } ) ;
402+ }
403+ }
404+
405+ // Fetch card details for all referenced cards
406+ const cardDetailsMap = new Map < number , any > ( ) ;
407+ if ( allCardIds . size > 0 ) {
408+ const cardDetails = await Promise . all (
409+ Array . from ( allCardIds ) . map ( async cardId => {
410+ try {
411+ const cardDetail = await fetchCard ( { card_id : cardId } ) ;
412+ return { id : cardId , detail : cardDetail } ;
413+ } catch ( error ) {
414+ console . warn ( `Failed to fetch card ${ cardId } :` , error ) ;
415+ return { id : cardId , detail : null } ;
416+ }
417+ } )
418+ ) ;
419+
420+ cardDetails . forEach ( ( { id, detail } ) => {
421+ if ( detail ) cardDetailsMap . set ( id , detail ) ;
422+ } ) ;
423+ }
424+
425+ filteredCards = await Promise . all (
426+ filteredCards . map ( card => processMBQLCard ( card , dbId , cardDetailsMap ) )
427+ ) ;
428+
429+ } catch ( error ) {
430+ console . error ( 'Error processing MBQL cards:' , error ) ;
431+ }
432+ }
265433 dashboardAppState . cards = filteredCards as SavedCard [ ] ;
266434 dashboardAppState . limitedEntities = uniqueEntities ;
267435 dashboardAppState . parameterValues = dashboardMetabaseState . parameterValues || { } ;
268436 return dashboardAppState ;
269437}
270438
439+ function splitAndTrimSQL ( sql : string ) : string {
440+ return sql . split ( '\n' ) . map ( part => part . trim ( ) ) . join ( '\n' )
441+ }
442+
443+ // This function extracts the contents of the outermost round brackets in a SQL string. i.e: The contents within the outermost ()
444+ function getOutermostParenthesesContent ( sql : string ) : string {
445+ let depth = 0 ;
446+ let start = - 1 ;
447+
448+ for ( let i = 0 ; i < sql . length ; i ++ ) {
449+ if ( sql [ i ] === '(' ) {
450+ if ( depth === 0 ) start = i + 1 ; // mark after '('
451+ depth ++ ;
452+ } else if ( sql [ i ] === ')' ) {
453+ depth -- ;
454+ if ( depth === 0 && start !== - 1 ) {
455+ return sql . slice ( start , i ) . trim ( ) ; // return without outer ()
456+ }
457+ }
458+ }
459+ return sql
460+ }
461+
462+ // function that goes through a nested object and returns all values for key "source-table"
463+ function getSourceTableIdsFromObject ( obj : any ) : any [ ] {
464+ let ids : number [ ] = [ ] ;
465+ if ( Array . isArray ( obj ) ) {
466+ for ( const item of obj ) {
467+ ids = ids . concat ( getSourceTableIdsFromObject ( item ) ) ;
468+ }
469+ } else if ( typeof obj === 'object' && obj !== null ) {
470+ if ( obj . hasOwnProperty ( 'source-table' ) ) {
471+ ids . push ( obj [ 'source-table' ] ) ;
472+ }
473+ for ( const key in obj ) {
474+ if ( obj . hasOwnProperty ( key ) ) {
475+ ids = ids . concat ( getSourceTableIdsFromObject ( obj [ key ] ) ) ;
476+ }
477+ }
478+ }
479+ return ids ;
480+ }
481+
271482
272483// export async function getDashboardInfoForModelling(): Promise<DashboardInfoForModelling | undefined> {
273484// const dashboardMetabaseState: DashboardMetabaseState = await getDashboardState() as DashboardMetabaseState;
0 commit comments