drizzle-cube
Version:
Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.
498 lines (497 loc) • 17.1 kB
TypeScript
import { ReactNode } from 'react';
import { ColorPalette } from './utils/colorPalettes';
import { FunnelBindingKey } from './types/funnel';
import { FlowChartData } from './types/flow';
export interface CubeMetaField {
name: string;
title: string;
shortTitle: string;
type: string;
}
export interface CubeMetaRelationship {
targetCube: string;
relationship: 'belongsTo' | 'hasOne' | 'hasMany' | 'belongsToMany';
joinFields?: Array<{
sourceField: string;
targetField: string;
}>;
}
export interface CubeMetaCube {
name: string;
title: string;
description?: string;
measures: CubeMetaField[];
dimensions: CubeMetaField[];
segments: CubeMetaField[];
relationships?: CubeMetaRelationship[];
/** Additional cube metadata (e.g., eventStream configuration for funnel queries) */
meta?: {
eventStream?: {
bindingKey: string;
timeDimension: string;
};
[key: string]: any;
};
}
export interface CubeMeta {
cubes: CubeMetaCube[];
}
export type FieldLabelMap = Record<string, string>;
export type { ColorPalette } from './utils/colorPalettes';
export type ChartType = 'line' | 'bar' | 'pie' | 'table' | 'area' | 'scatter' | 'radar' | 'radialBar' | 'treemap' | 'bubble' | 'activityGrid' | 'kpiNumber' | 'kpiDelta' | 'kpiText' | 'markdown' | 'funnel' | 'sankey' | 'sunburst';
export interface AxisFormatConfig {
label?: string;
unit?: 'currency' | 'percent' | 'number' | 'custom';
abbreviate?: boolean;
decimals?: number;
customPrefix?: string;
customSuffix?: string;
}
export interface ChartAxisConfig {
xAxis?: string[];
yAxis?: string[];
series?: string[];
sizeField?: string;
colorField?: string;
dateField?: string[];
valueField?: string[];
x?: string;
y?: string[];
yAxisAssignment?: Record<string, 'left' | 'right'>;
}
export interface ChartDisplayConfig {
showLegend?: boolean;
showGrid?: boolean;
showTooltip?: boolean;
colors?: string[];
orientation?: 'horizontal' | 'vertical';
stacked?: boolean;
stackType?: 'none' | 'normal' | 'percent';
connectNulls?: boolean;
hideHeader?: boolean;
minBubbleSize?: number;
maxBubbleSize?: number;
bubbleOpacity?: number;
showLabels?: boolean;
fitToWidth?: boolean;
pivotTimeDimension?: boolean;
target?: string;
template?: string;
prefix?: string;
suffix?: string;
decimals?: number;
formatValue?: (value: number | null | undefined) => string;
valueColor?: string;
valueColorIndex?: number;
positiveColorIndex?: number;
negativeColorIndex?: number;
showHistogram?: boolean;
useLastCompletePeriod?: boolean;
skipLastPeriod?: boolean;
content?: string;
accentColorIndex?: number;
fontSize?: 'small' | 'medium' | 'large';
alignment?: 'left' | 'center' | 'right';
xAxisFormat?: AxisFormatConfig;
leftYAxisFormat?: AxisFormatConfig;
rightYAxisFormat?: AxisFormatConfig;
/**
* How to display compared periods:
* - 'separate': Each period as distinct series with different colors (default)
* - 'overlay': Periods aligned by day-of-period index with ghost styling for prior periods
*/
comparisonMode?: 'separate' | 'overlay';
/** Line style for prior periods in overlay mode */
priorPeriodStyle?: 'solid' | 'dashed' | 'dotted';
/** Opacity for prior period lines (0-1), default: 0.5 */
priorPeriodOpacity?: number;
/** Include period labels in legend */
showPeriodLabels?: boolean;
/** Custom labels for funnel steps (array indexed by step, e.g., ["Signup", "Activation", "Purchase"]) */
funnelStepLabels?: string[];
/** Hide the summary footer in funnel charts */
hideSummaryFooter?: boolean;
/** Funnel orientation: horizontal (bars left to right) or vertical (bars bottom to top) */
funnelOrientation?: 'horizontal' | 'vertical';
/** @deprecated Use showFunnelAvgTime, showFunnelMedianTime, showFunnelP90Time instead */
showFunnelTimeMetrics?: boolean;
/** Funnel visualization style: 'bars' (horizontal bars) or 'funnel' (trapezoid funnel shape) */
funnelStyle?: 'bars' | 'funnel';
/** Show step-to-step conversion rate (default: true) */
showFunnelConversion?: boolean;
/** Show average time-to-convert metric in funnel charts */
showFunnelAvgTime?: boolean;
/** Show median time-to-convert metric in funnel charts */
showFunnelMedianTime?: boolean;
/** Show P90 time-to-convert metric in funnel charts */
showFunnelP90Time?: boolean;
}
export interface PortletConfig {
id: string;
title: string;
/**
* Canonical format for analysis configuration.
* This is the single source of truth for all query/chart config.
* New portlets only save this field. Legacy portlets are migrated on-the-fly.
*/
analysisConfig?: import('./types/analysisConfig').AnalysisConfig;
/** @deprecated Use analysisConfig.query instead */
query?: string;
/** @deprecated Use analysisConfig.charts[mode].chartType instead */
chartType?: ChartType;
/** @deprecated Use analysisConfig.charts[mode].chartConfig instead */
chartConfig?: ChartAxisConfig;
/** @deprecated Use analysisConfig.charts[mode].displayConfig instead */
displayConfig?: ChartDisplayConfig;
dashboardFilterMapping?: string[];
eagerLoad?: boolean;
/** @deprecated Use analysisConfig.analysisType instead */
analysisType?: AnalysisType;
/** @deprecated Use analysisConfig for funnel mode */
funnelCube?: string | null;
/** @deprecated Use analysisConfig for funnel mode */
funnelSteps?: FunnelStepState[];
/** @deprecated Use analysisConfig for funnel mode */
funnelTimeDimension?: string | null;
/** @deprecated Use analysisConfig for funnel mode */
funnelBindingKey?: FunnelBindingKey | null;
/** @deprecated Use analysisConfig for funnel mode */
funnelChartType?: ChartType;
/** @deprecated Use analysisConfig for funnel mode */
funnelChartConfig?: ChartAxisConfig;
/** @deprecated Use analysisConfig for funnel mode */
funnelDisplayConfig?: ChartDisplayConfig;
w: number;
h: number;
x: number;
y: number;
}
export type DashboardLayoutMode = 'grid' | 'rows';
export interface DashboardGridSettings {
cols: number;
rowHeight: number;
minW: number;
minH: number;
}
export interface RowLayoutColumn {
portletId: string;
w: number;
}
export interface RowLayout {
id: string;
h: number;
columns: RowLayoutColumn[];
}
export interface DashboardConfig {
portlets: PortletConfig[];
layoutMode?: DashboardLayoutMode;
grid?: DashboardGridSettings;
rows?: RowLayout[];
layouts?: {
[key: string]: any;
};
colorPalette?: string;
filters?: DashboardFilter[];
eagerLoad?: boolean;
}
export type AnalysisType = 'query' | 'funnel' | 'flow';
/**
* State for a single funnel step (dedicated for Funnel mode)
* Each step represents a stage in the funnel with its own cube and filters
*/
export interface FunnelStepState {
/** Unique step identifier */
id: string;
/** Display name for the step (e.g., "Signup", "Purchase") */
name: string;
/** Which cube this step uses (for multi-cube funnels) */
cube: string;
/** Filters that define which events qualify for this step */
filters: Filter[];
/** Time window from previous step (ISO 8601 duration, e.g., "P7D" for 7 days) */
timeToConvert?: string;
}
export type FilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'notStartsWith' | 'endsWith' | 'notEndsWith' | 'like' | 'notLike' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'notBetween' | 'in' | 'notIn' | 'arrayContains' | 'arrayOverlaps' | 'arrayContained' | 'set' | 'notSet' | 'isEmpty' | 'isNotEmpty' | 'inDateRange' | 'beforeDate' | 'afterDate' | 'regex' | 'notRegex';
export interface SimpleFilter {
member: string;
operator: FilterOperator;
values: any[];
dateRange?: string | string[];
}
export interface GroupFilter {
type: 'and' | 'or';
filters: Filter[];
}
export type Filter = SimpleFilter | GroupFilter;
export interface DashboardFilter {
id: string;
label: string;
filter: Filter;
isUniversalTime?: boolean;
}
export interface CubeQuery {
measures?: string[];
dimensions?: string[];
timeDimensions?: Array<{
dimension: string;
granularity?: string;
dateRange?: string[] | string;
fillMissingDates?: boolean;
/**
* Array of date ranges for period-over-period comparison.
* When specified, queries are executed for each period and results are merged.
*/
compareDateRange?: (string | [string, string])[];
}>;
filters?: Filter[];
order?: {
[key: string]: 'asc' | 'desc';
};
limit?: number;
offset?: number;
segments?: string[];
}
/**
* Merge strategy for combining multiple query results
* - 'concat': Append rows with __queryIndex marker (for separate series per query)
* - 'merge': Align data by common dimension key (for combined visualization)
*
* Note: For funnel analysis, use the dedicated funnel mode (analysisType === 'funnel')
*/
export type QueryMergeStrategy = 'concat' | 'merge';
/**
* Configuration for multi-query portlets
* Detected by presence of 'queries' array property
*
* Note: For funnel analysis, use the dedicated funnel mode (analysisType === 'funnel')
*/
export interface MultiQueryConfig {
queries: CubeQuery[];
mergeStrategy: QueryMergeStrategy;
mergeKeys?: string[];
queryLabels?: string[];
}
/**
* Type guard to detect multi-query configuration
*/
export declare function isMultiQueryConfig(obj: unknown): obj is MultiQueryConfig;
export interface CubeQueryOptions {
skip?: boolean;
resetResultSetOnChange?: boolean;
subscribe?: boolean;
}
export interface CubeApiOptions {
apiUrl?: string;
token?: string;
headers?: Record<string, string>;
credentials?: 'include' | 'omit' | 'same-origin';
}
export interface CubeResultSet {
rawData(): any[];
tablePivot(): any[];
series(): any[];
annotation(): any;
loadResponse?: any;
cacheInfo?(): {
hit: true;
cachedAt: string;
ttlMs: number;
ttlRemainingMs: number;
} | undefined;
}
export interface AnalyticsPortletProps {
query: string;
chartType: ChartType;
chartConfig?: ChartAxisConfig;
displayConfig?: ChartDisplayConfig;
dashboardFilters?: DashboardFilter[];
dashboardFilterMapping?: string[];
eagerLoad?: boolean;
isVisible?: boolean;
height?: string | number;
title?: string;
colorPalette?: ColorPalette;
loadingComponent?: ReactNode;
onDebugDataReady?: (debugData: {
chartConfig: ChartAxisConfig;
displayConfig: ChartDisplayConfig;
queryObject: any;
data: any[] | FlowChartData;
chartType: ChartType;
cacheInfo?: {
hit: true;
cachedAt: string;
ttlMs: number;
ttlRemainingMs: number;
} | null;
}) => void;
}
export interface AnalyticsDashboardProps {
config: DashboardConfig;
editable?: boolean;
dashboardFilters?: DashboardFilter[];
loadingComponent?: ReactNode;
onConfigChange?: (config: DashboardConfig) => void;
onSave?: (config: DashboardConfig) => Promise<void> | void;
onDirtyStateChange?: (isDirty: boolean) => void;
}
export interface ChartProps {
data: any[];
chartConfig?: ChartAxisConfig;
displayConfig?: ChartDisplayConfig;
queryObject?: CubeQuery;
height?: string | number;
colorPalette?: ColorPalette;
}
export interface FeaturesConfig {
enableAI?: boolean;
aiEndpoint?: string;
showSchemaDiagram?: boolean;
useAnalysisBuilder?: boolean;
editToolbar?: 'floating' | 'top' | 'both';
floatingToolbarPosition?: 'left' | 'right';
}
export interface GridLayout {
i: string;
x: number;
y: number;
w: number;
h: number;
minW?: number;
minH?: number;
}
export interface ResponsiveLayout {
[breakpoint: string]: GridLayout[];
}
export type DashboardDisplayMode = 'desktop' | 'scaled' | 'mobile';
export type { FunnelBindingKey, FunnelBindingKeyMapping, FunnelStep, FunnelConfig, FunnelStepResult, FunnelExecutionResult, FunnelChartData, FunnelValidationError, FunnelValidationResult, UseFunnelQueryOptions, UseFunnelQueryResult, ServerFunnelQuery, } from './types/funnel';
/**
* Type guard to detect server funnel query format
* Used to distinguish { funnel: {...} } from CubeQuery or MultiQueryConfig
*/
export declare function isServerFunnelQuery(obj: unknown): obj is import('./types/funnel').ServerFunnelQuery;
export type { FlowStartingStep, ServerFlowQuery, FlowQueryConfig, SankeyNode, SankeyLink, FlowResultRow, FlowChartData, FlowSliceState, FlowSliceActions, } from './types/flow';
export { isServerFlowQuery, isSankeyData } from './types/flow';
/**
* Options for EXPLAIN query execution
*/
export interface ExplainOptions {
/** Use EXPLAIN ANALYZE to actually execute the query and get real timing (PostgreSQL, MySQL 8.0.18+) */
analyze?: boolean;
}
/**
* A single operation/node in the query execution plan
* Normalized structure across all databases
*/
export interface ExplainOperation {
/** Operation type (e.g., 'Seq Scan', 'Index Scan', 'Hash Join', 'Sort') */
type: string;
/** Table name if applicable */
table?: string;
/** Index name if used */
index?: string;
/** Estimated row count from planner */
estimatedRows?: number;
/** Actual row count (if ANALYZE was used) */
actualRows?: number;
/** Estimated cost (database-specific units) */
estimatedCost?: number;
/** Filter condition if any */
filter?: string;
/** Additional details specific to this operation */
details?: string;
/** Nested/child operations */
children?: ExplainOperation[];
}
/**
* Summary statistics from the execution plan
*/
export interface ExplainSummary {
/** Database engine type */
database: 'postgres' | 'mysql' | 'sqlite';
/** Planning time in milliseconds (if available) */
planningTime?: number;
/** Execution time in milliseconds (if ANALYZE was used) */
executionTime?: number;
/** Total estimated cost */
totalCost?: number;
/** Quick flag: true if any sequential scans detected */
hasSequentialScans: boolean;
/** List of indexes used in the plan */
usedIndexes: string[];
}
/**
* Result of an EXPLAIN query
* Provides both normalized structure and raw output
*/
export interface ExplainResult {
/** Normalized hierarchical plan as operations */
operations: ExplainOperation[];
/** Summary statistics */
summary: ExplainSummary;
/** Raw EXPLAIN output as text (for display) */
raw: string;
/** Original SQL query */
sql: {
sql: string;
params?: unknown[];
};
}
/**
* A recommendation from AI analysis of an execution plan
*/
export interface ExplainRecommendation {
/** Type of recommendation */
type: 'index' | 'table' | 'cube' | 'general';
/** Severity/priority of the recommendation */
severity: 'critical' | 'warning' | 'suggestion';
/** Short actionable title */
title: string;
/** Detailed explanation of why this helps */
description: string;
/** Actionable SQL statement (e.g., CREATE INDEX) - for index/table recommendations */
sql?: string;
/** TypeScript code snippet to add to cube definition - for cube recommendations */
cubeCode?: string;
/** Which cube to modify - for cube recommendations */
cubeName?: string;
/** Affected database table */
table?: string;
/** Affected columns */
columns?: string[];
/** Expected performance improvement */
estimatedImpact?: string;
}
/**
* Issue identified in the execution plan
*/
export interface ExplainIssue {
/** Type of issue */
type: 'sequential_scan' | 'missing_index' | 'high_cost' | 'sort_operation' | string;
/** Description of the issue */
description: string;
/** Severity level */
severity: 'high' | 'medium' | 'low';
}
/**
* AI-generated analysis of an execution plan
*/
export interface AIExplainAnalysis {
/** One-sentence description of what the query does */
summary: string;
/** Overall performance assessment */
assessment: 'good' | 'warning' | 'critical';
/** Reason for the assessment */
assessmentReason: string;
/** Detailed explanation of the query's purpose and structure */
queryUnderstanding: string;
/** Issues identified in the execution plan */
issues: ExplainIssue[];
/** Actionable recommendations for improvement */
recommendations: ExplainRecommendation[];
/** Metadata from the AI analysis */
_meta?: {
model: string;
usingUserKey: boolean;
};
}