Skip to content

Config Migration Guide

This guide helps you migrate from the legacy portlet configuration format to the new AnalysisConfig format introduced in drizzle-cube 0.3.0.

The new AnalysisConfig format provides:

  • Unified persistence - Single format for localStorage, share URLs, and portlets
  • Multi-mode support - Separate configurations for query and funnel analysis
  • Version tracking - Future-proof migrations with version field
  • Cleaner structure - Chart config organized by mode in charts map
ChangeLegacyNewImpact
Chart config locationTop-level chartTypecharts[mode].chartTypeTypeScript errors, runtime access changes
Query storageJSON stringParsed objectApps parsing JSON.parse(query) may break
Version fieldNoneRequired version: 1Validation may fail
Analysis typeImplicitExplicit analysisTypeMust specify mode
Chart per modeSingle configcharts map by modeDifferent access pattern

The old portlet format stored configuration like this:

// Legacy PortletConfig (pre-0.3.0)
interface LegacyPortlet {
id: string
title: string
// Query as JSON string
query: string // JSON.stringify({ measures: [...], dimensions: [...] })
// Chart config at top level
chartType: 'bar' | 'line' | 'table' | ...
chartConfig?: {
xAxis?: string[]
yAxis?: string[]
}
displayConfig?: {
showLegend?: boolean
}
// Layout
w: number
h: number
x: number
y: number
}
const legacyPortlet = {
id: 'revenue-chart',
title: 'Monthly Revenue',
query: JSON.stringify({
measures: ['Orders.totalRevenue'],
timeDimensions: [{
dimension: 'Orders.createdAt',
granularity: 'month'
}]
}),
chartType: 'line',
chartConfig: {
xAxis: ['Orders.createdAt'],
yAxis: ['Orders.totalRevenue']
},
displayConfig: {
showLegend: true
},
w: 6, h: 4, x: 0, y: 0
}

The new format uses AnalysisConfig for query/chart configuration:

// New PortletConfig (0.3.0+)
interface PortletConfig {
id: string
title: string
// New: Canonical config format
analysisConfig: AnalysisConfig
// Layout (unchanged)
w: number
h: number
x: number
y: number
// Legacy fields (kept for backward compatibility during transition)
query?: string
chartType?: ChartType
chartConfig?: ChartAxisConfig
displayConfig?: ChartDisplayConfig
}
const newPortlet = {
id: 'revenue-chart',
title: 'Monthly Revenue',
analysisConfig: {
version: 1,
analysisType: 'query',
activeView: 'chart',
charts: {
query: {
chartType: 'line',
chartConfig: {
xAxis: ['Orders.createdAt'],
yAxis: ['Orders.totalRevenue']
},
displayConfig: {
showLegend: true
}
}
},
query: {
measures: ['Orders.totalRevenue'],
timeDimensions: [{
dimension: 'Orders.createdAt',
granularity: 'month'
}]
}
},
w: 6, h: 4, x: 0, y: 0
}

drizzle-cube includes migration utilities that automatically convert legacy formats:

import { migrateConfig, isValidAnalysisConfig } from 'drizzle-cube/client'
// Works with any format
const config = migrateConfig(unknownData)
// Returns valid AnalysisConfig
// Check before migrating
if (!isValidAnalysisConfig(data)) {
const migrated = migrateConfig(data)
// Use migrated config
}
import { migrateLegacyPortlet } from 'drizzle-cube/client'
const legacyPortlet = {
query: '{"measures":["Orders.count"]}',
chartType: 'bar',
chartConfig: { yAxis: ['Orders.count'] }
}
const analysisConfig = migrateLegacyPortlet(legacyPortlet)
// Returns QueryAnalysisConfig

Legacy:

{
query: '{"measures":["Employees.count"],"dimensions":["Employees.department"]}',
chartType: 'bar',
chartConfig: { xAxis: ['Employees.department'], yAxis: ['Employees.count'] }
}

Migrated:

{
version: 1,
analysisType: 'query',
activeView: 'chart',
charts: {
query: {
chartType: 'bar',
chartConfig: { xAxis: ['Employees.department'], yAxis: ['Employees.count'] },
displayConfig: {}
}
},
query: {
measures: ['Employees.count'],
dimensions: ['Employees.department']
}
}

Legacy:

{
query: JSON.stringify({
queries: [
{ measures: ['Sales.revenue'], filters: [...] },
{ measures: ['Sales.revenue'], filters: [...] }
],
mergeStrategy: 'concat',
queryLabels: ['Region A', 'Region B']
}),
chartType: 'line'
}

Migrated:

{
version: 1,
analysisType: 'query',
activeView: 'chart',
charts: {
query: { chartType: 'line', chartConfig: {}, displayConfig: {} }
},
query: {
queries: [
{ measures: ['Sales.revenue'], filters: [...] },
{ measures: ['Sales.revenue'], filters: [...] }
],
mergeStrategy: 'concat',
queryLabels: ['Region A', 'Region B']
}
}

Scenario 3: Legacy Funnel (mergeStrategy: ‘funnel’)

Section titled “Scenario 3: Legacy Funnel (mergeStrategy: ‘funnel’)”

The old funnel pattern used multi-query with mergeStrategy: 'funnel':

Legacy:

{
query: JSON.stringify({
queries: [
{ measures: ['Events.count'], filters: [{ member: 'Events.type', operator: 'equals', values: ['signup'] }] },
{ measures: ['Events.count'], filters: [{ member: 'Events.type', operator: 'equals', values: ['purchase'] }] }
],
mergeStrategy: 'funnel',
queryLabels: ['Signup', 'Purchase'],
funnelBindingKey: { dimension: 'Events.userId' }
}),
chartType: 'funnel'
}

Migrated:

{
version: 1,
analysisType: 'funnel',
activeView: 'chart',
charts: {
funnel: { chartType: 'funnel', chartConfig: {}, displayConfig: {} }
},
query: {
funnel: {
bindingKey: 'Events.userId',
timeDimension: '', // Extracted from first query if available
steps: [
{ name: 'Signup', filter: { member: 'Events.type', operator: 'equals', values: ['signup'] } },
{ name: 'Purchase', filter: { member: 'Events.type', operator: 'equals', values: ['purchase'] } }
],
includeTimeMetrics: true
}
}
}

If the query is already a ServerFunnelQuery, it’s preserved as-is:

Legacy:

{
query: JSON.stringify({
funnel: {
bindingKey: 'Events.userId',
timeDimension: 'Events.timestamp',
steps: [...]
}
}),
chartType: 'funnel'
}

Migrated:

{
version: 1,
analysisType: 'funnel',
activeView: 'chart',
charts: {
funnel: { chartType: 'funnel', chartConfig: {}, displayConfig: {} }
},
query: {
funnel: {
bindingKey: 'Events.userId',
timeDimension: 'Events.timestamp',
steps: [...]
}
}
}

If you need to migrate programmatically:

function parseLegacyQuery(portlet: LegacyPortlet) {
try {
return JSON.parse(portlet.query)
} catch {
return { measures: [], dimensions: [] }
}
}
function determineAnalysisType(query: unknown, portlet: LegacyPortlet): 'query' | 'funnel' {
// Check if it's a ServerFunnelQuery
if (query && typeof query === 'object' && 'funnel' in query) {
return 'funnel'
}
// Check if it's a legacy funnel multi-query
if (query && 'mergeStrategy' in query && query.mergeStrategy === 'funnel') {
return 'funnel'
}
// Check explicit analysisType
if (portlet.analysisType === 'funnel') {
return 'funnel'
}
return 'query'
}
function buildAnalysisConfig(portlet: LegacyPortlet): AnalysisConfig {
const query = parseLegacyQuery(portlet)
const analysisType = determineAnalysisType(query, portlet)
const chartConfig: ChartConfig = {
chartType: portlet.chartType || (analysisType === 'funnel' ? 'funnel' : 'bar'),
chartConfig: portlet.chartConfig || {},
displayConfig: portlet.displayConfig || {}
}
return {
version: 1,
analysisType,
activeView: 'chart',
charts: {
[analysisType]: chartConfig
},
query
} as AnalysisConfig
}

Before:

const chartType = portlet.chartType

After:

const chartType = portlet.analysisConfig?.charts[portlet.analysisConfig.analysisType]?.chartType
?? portlet.chartType // Fallback for legacy

Before:

const query = JSON.parse(portlet.query)

After:

const query = portlet.analysisConfig?.query
?? JSON.parse(portlet.query) // Fallback for legacy

Before:

const updated = {
...portlet,
chartType: 'line',
query: JSON.stringify(newQuery)
}

After:

const updated = {
...portlet,
analysisConfig: {
...portlet.analysisConfig,
charts: {
...portlet.analysisConfig.charts,
[portlet.analysisConfig.analysisType]: {
...portlet.analysisConfig.charts[portlet.analysisConfig.analysisType],
chartType: 'line'
}
},
query: newQuery
}
}

If you have portlet configs stored in a database, you can migrate them:

async function migrateDatabaseConfigs(db: Database) {
const dashboards = await db.query('SELECT * FROM analytics_pages')
for (const dashboard of dashboards) {
const config = JSON.parse(dashboard.config)
const migratedPortlets = config.portlets.map(portlet => {
// Skip if already migrated
if (portlet.analysisConfig) return portlet
return {
...portlet,
analysisConfig: migrateLegacyPortlet(portlet)
}
})
await db.query(
'UPDATE analytics_pages SET config = ? WHERE id = ?',
[JSON.stringify({ ...config, portlets: migratedPortlets }), dashboard.id]
)
}
}

Always validate configs before use:

import { isValidAnalysisConfig, migrateConfig } from 'drizzle-cube/client'
function ensureValidConfig(data: unknown): AnalysisConfig {
if (isValidAnalysisConfig(data)) {
return data
}
const migrated = migrateConfig(data)
if (!isValidAnalysisConfig(migrated)) {
throw new Error('Failed to migrate config')
}
return migrated
}

This occurs when migrateConfig() can’t recognize the input format:

// Check what you're passing
console.log('Config type:', typeof config)
console.log('Config:', JSON.stringify(config, null, 2))
// Ensure it's an object with expected fields
if (typeof config !== 'object' || config === null) {
// Handle invalid input
}

If chart settings are missing, check if the legacy portlet had the correct fields:

const legacyPortlet = {
chartType: 'bar', // Required
chartConfig: { ... }, // Optional
displayConfig: { ... } // Optional
}

Ensure the legacy format has proper funnel indicators:

// These trigger funnel detection:
{ mergeStrategy: 'funnel' } // Legacy multi-query funnel
{ funnel: { ... } } // ServerFunnelQuery
{ analysisType: 'funnel' } // Explicit type