UNPKG

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
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; }; }