Skip to content

caochun/mintbase

Repository files navigation

Mintbase - 简化版 Metabase

一个轻量级的 BI 工具,专注于核心功能,易于理解和扩展。

Mintbase Logo

📋 目录


项目简介

Mintbase 是 Metabase 的简化实现版本,旨在提供一个易于理解、便于学习的 BI 工具实现。它保留了 Metabase 的核心功能,同时大幅简化了架构和代码复杂度。

项目定位

  • 教育目的: 帮助开发者理解 BI 工具的核心原理
  • 快速原型: 适合作为定制化 BI 工具的基础
  • 轻量级: 代码简洁,易于维护和扩展
  • 非生产级: 不适合直接用于生产环境(缺少企业级特性)

与 Metabase 的对比

特性 Metabase Mintbase
语言 Clojure (JVM) Node.js (JavaScript)
前端框架 React + Redux 原生 JS + Tailwind CSS
查询语言 完整 MBQL 简化 MBQL
数据库支持 50+ SQLite (可扩展)
Collections
Models
Metrics
复杂权限
学习难度

核心特性

✅ 已实现功能

  1. 数据源管理

    • SQLite 数据库连接
    • 自动元数据同步(表、字段、类型)
    • ER 图可视化(基于 Mermaid.js)
    • 外键关系识别
  2. 可视化查询构建器

    • 选择数据表
    • 智能字段选择(基于聚合/分组动态更新)
    • 多表 JOIN(支持智能推荐)
    • 过滤条件构建
    • 聚合函数(Count, Sum, Avg, Min, Max)
    • 分组(Breakout)
    • 自定义表达式(算术运算)
    • 排序
    • 结果限制
  3. 数据可视化

    • 数据表格视图
    • 柱状图
    • 折线图
    • 饼图
    • 数字卡片(大数字显示)
    • 实时图表类型切换
  4. 问题(Questions)管理

    • 保存查询为问题
    • 问题列表和搜索
    • 问题编辑和删除
    • 查询结果导出
  5. 仪表板(Dashboards)

    • 创建和管理仪表板
    • 添加问题到仪表板
    • 网格布局系统
    • 仪表板查看和刷新

🚧 计划中功能

  • 更多数据库支持(PostgreSQL, MySQL)
  • Native SQL 编辑器
  • 数据权限管理
  • 用户认证和授权
  • 定时刷新和缓存
  • 更多图表类型

技术架构

架构概览

┌─────────────────────────────────────────────────────────────┐
│                         前端层                               │
│  ┌────────────┐  ┌──────────────┐  ┌──────────────┐        │
│  │ 查询构建器  │  │  可视化引擎  │  │  仪表板管理  │        │
│  │ (原生 JS)  │  │  (Chart.js)  │  │   (原生 JS)  │        │
│  └────────────┘  └──────────────┘  └──────────────┘        │
│         │                │                  │                │
│         └────────────────┴──────────────────┘                │
│                          │                                   │
│                    HTTP (JSON)                               │
└─────────────────────────┼───────────────────────────────────┘
                          │
┌─────────────────────────┼───────────────────────────────────┐
│                      后端层 (Express)                        │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                   API 路由层                          │   │
│  │  /api/databases  /api/cards  /api/dashboards         │   │
│  │  /api/query      /api/tables /api/fields             │   │
│  └──────────────────┬───────────────────────────────────┘   │
│                     │                                        │
│  ┌──────────────────┴───────────────────────────────────┐   │
│  │               查询处理层                              │   │
│  │  ┌─────────────┐  ┌────────────┐  ┌──────────────┐  │   │
│  │  │ MBQL 解析器 │→ │ SQL 转换器 │→ │ 查询执行器   │  │   │
│  │  └─────────────┘  └────────────┘  └──────────────┘  │   │
│  └──────────────────┬───────────────────────────────────┘   │
│                     │                                        │
│  ┌──────────────────┴───────────────────────────────────┐   │
│  │                数据访问层                             │   │
│  │  ┌──────────────┐  ┌──────────────┐                  │   │
│  │  │  模型层      │  │  驱动层       │                  │   │
│  │  │ (Card/DB)    │  │ (SQLite)      │                  │   │
│  │  └──────────────┘  └──────────────┘                  │   │
│  └──────────────────────────────────────────────────────┘   │
└──────────────────────────┬───────────────────────────────────┘
                           │
┌──────────────────────────┴───────────────────────────────────┐
│                      数据库层                                 │
│  ┌──────────────────┐      ┌──────────────────┐             │
│  │  应用数据库       │      │  目标数据库       │             │
│  │  (mintbase.db)   │      │  (northwind.db)  │             │
│  │  - Cards         │      │  - Customers     │             │
│  │  - Dashboards    │      │  - Orders        │             │
│  │  - Databases     │      │  - Products      │             │
│  └──────────────────┘      └──────────────────┘             │
└──────────────────────────────────────────────────────────────┘

技术选型

后端技术栈

技术 版本 用途
Node.js 18+ 运行时环境
Express 4.x Web 框架
SQLite3 5.x 数据库驱动
better-sqlite3 9.x 同步 SQLite API
dotenv 16.x 环境变量管理

为什么选择 Node.js?

  1. 开发效率: JavaScript 语法简洁,无需编译,热重载开发体验好
  2. 前后端统一: 前后端都使用 JavaScript,降低学习成本
  3. JSON 原生支持: 查询语言是 JSON 格式,处理更自然
  4. 轻量级: 适合快速原型开发和迭代
  5. 生态丰富: npm 生态系统提供丰富的第三方库

前端技术栈

技术 版本 用途
原生 JavaScript ES6+ 业务逻辑
Tailwind CSS 3.x UI 样式
Chart.js 4.x 图表渲染
Mermaid.js 10.x ER 图渲染
Choices.js 10.x 多选下拉框

为什么不用 React/Vue?

  1. 学习曲线: 原生 JS 更容易理解,适合教学
  2. 代码透明: 没有框架魔法,所有逻辑清晰可见
  3. 零构建: 不需要复杂的构建工具链
  4. 轻量级: 页面加载更快

核心模块

1. 查询处理流程

用户操作
   ↓
构建 MBQL JSON
   ↓
发送到后端 /api/query
   ↓
验证查询 (validator.js)
   ↓
转换为 SQL (mbql-to-sql.js)
   ↓
执行查询 (executor.js)
   ↓
返回结果
   ↓
前端渲染(表格/图表)

2. 数据模型

应用数据模型 (存储在 mintbase.db):

-- 数据库连接配置
CREATE TABLE databases (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    engine TEXT NOT NULL,
    details TEXT NOT NULL  -- JSON: {dbPath, ...}
);

-- 数据表元数据
CREATE TABLE tables (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    database_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    schema TEXT,
    FOREIGN KEY (database_id) REFERENCES databases(id)
);

-- 字段元数据
CREATE TABLE fields (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    table_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    base_type TEXT NOT NULL,  -- 'type/Text', 'type/Integer', etc.
    semantic_type TEXT,        -- 'type/PK', 'type/FK', etc.
    FOREIGN KEY (table_id) REFERENCES tables(id)
);

-- 保存的问题
CREATE TABLE cards (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT,
    dataset_query TEXT NOT NULL,  -- JSON: MBQL 查询
    display TEXT NOT NULL,         -- 'table', 'bar', 'line', etc.
    visualization_settings TEXT,   -- JSON: 可视化配置
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 仪表板
CREATE TABLE dashboards (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 仪表板卡片
CREATE TABLE dashboard_cards (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dashboard_id INTEGER NOT NULL,
    card_id INTEGER NOT NULL,
    row INTEGER NOT NULL,
    col INTEGER NOT NULL,
    size_x INTEGER NOT NULL,
    size_y INTEGER NOT NULL,
    FOREIGN KEY (dashboard_id) REFERENCES dashboards(id),
    FOREIGN KEY (card_id) REFERENCES cards(id)
);

查询语言设计

Mintbase 使用简化版的 MBQL (Metabase Query Language),这是一种基于 JSON 的声明式查询语言。

MBQL 核心概念

MBQL 的设计哲学:

  1. 声明式: 描述"要什么"而不是"怎么做"
  2. JSON 格式: 易于序列化和传输
  3. 数据库无关: 可转换为不同数据库的 SQL
  4. 可组合: 各个部分可以独立组合

基础查询结构

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 1,
    "fields": [...],
    "joins": [...],
    "filter": [...],
    "aggregation": [...],
    "breakout": [...],
    "expressions": {...},
    "order-by": [...],
    "limit": 100
  }
}

字段引用

1. 普通字段引用

["field", 5, null]
  • "field": 字段类型标识符
  • 5: 字段 ID(在 fields 表中的 ID)
  • null: 字段选项(暂未使用)

2. JOIN 表字段引用

["field", 12, {"join-alias": "Products"}]
  • 引用 JOIN 的表中的字段
  • join-alias 指定 JOIN 的别名

3. 表达式引用

["expression", "total_price"]
  • 引用自定义表达式
  • 表达式必须在 expressions 中定义

过滤条件 (Filter)

基本比较运算符

// 等于
["=", ["field", 5, null], "John"]

// 不等于
["!=", ["field", 5, null], "John"]

// 大于
[">", ["field", 6, null], 100]

// 小于
["<", ["field", 6, null], 1000]

// 大于等于
[">=", ["field", 6, null], 100]

// 小于等于
["<=", ["field", 6, null], 1000]

逻辑运算符

// AND
["and",
  ["=", ["field", 5, null], "John"],
  [">", ["field", 6, null], 100]
]

// OR
["or",
  ["=", ["field", 5, null], "John"],
  ["=", ["field", 5, null], "Jane"]
]

// NOT
["not", ["=", ["field", 5, null], "John"]]

特殊运算符

// BETWEEN
["between", ["field", 6, null], 100, 1000]

// IN
["in", ["field", 5, null], "John", "Jane", "Bob"]

// IS NULL
["is-null", ["field", 5, null]]

// NOT NULL
["not-null", ["field", 5, null]]

// LIKE (模糊匹配)
["contains", ["field", 5, null], "john"]
["starts-with", ["field", 5, null], "john"]
["ends-with", ["field", 5, null], "smith"]

聚合函数 (Aggregation)

// 计数
[["count"]]

// 求和
[["sum", ["field", 6, null]]]

// 平均值
[["avg", ["field", 6, null]]]

// 最小值
[["min", ["field", 6, null]]]

// 最大值
[["max", ["field", 6, null]]]

// 多个聚合
[
  ["count"],
  ["sum", ["field", 6, null]],
  ["avg", ["field", 6, null]]
]

分组 (Breakout)

// 单字段分组
{
  "breakout": [["field", 5, null]],
  "aggregation": [["count"]]
}

// 多字段分组
{
  "breakout": [
    ["field", 5, null],
    ["field", 7, null]
  ],
  "aggregation": [["sum", ["field", 6, null]]]
}

自定义表达式 (Expressions)

{
  "expressions": {
    "total_price": ["+", ["field", 10, null], ["field", 11, null]],
    "discount_price": ["*", ["field", 10, null], 0.9]
  },
  "fields": [
    ["field", 9, null],
    ["expression", "total_price"],
    ["expression", "discount_price"]
  ]
}

支持的运算符:

  • +: 加法
  • -: 减法
  • *: 乘法
  • /: 除法

JOIN 操作

{
  "source-table": 1,
  "joins": [
    {
      "alias": "Products",
      "source-table": 2,
      "condition": [
        "=",
        ["field", 8, null],                    // 主表字段
        ["field", 15, {"join-alias": "Products"}]  // JOIN 表字段
      ]
    }
  ]
}

排序 (Order By)

// 单字段排序
{
  "order-by": [[["field", 5, null], "asc"]]
}

// 多字段排序
{
  "order-by": [
    [["field", 5, null], "desc"],
    [["field", 6, null], "asc"]
  ]
}

// 对聚合结果排序
{
  "aggregation": [["count"]],
  "order-by": [[["aggregation", 0], "desc"]]
}

字段选择 (Fields)

// 选择特定字段
{
  "source-table": 1,
  "fields": [
    ["field", 5, null],
    ["field", 6, null]
  ]
}

// 在聚合查询中选择字段(过滤分组字段)
{
  "aggregation": [["count"]],
  "breakout": [
    ["field", 5, null],
    ["field", 6, null]
  ],
  "fields": [
    ["field", 5, null]  // 只显示第一个分组字段
  ]
}

结果限制 (Limit)

{
  "source-table": 1,
  "limit": 100
}

完整查询示例

示例 1: 简单查询

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 1,
    "fields": [
      ["field", 1, null],
      ["field", 2, null]
    ],
    "filter": ["=", ["field", 3, null], "USA"],
    "limit": 10
  }
}

转换为 SQL:

SELECT "field_1", "field_2"
FROM "table_1"
WHERE "field_3" = 'USA'
LIMIT 10

示例 2: 聚合查询

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 2,
    "aggregation": [
      ["count"],
      ["sum", ["field", 10, null]]
    ],
    "breakout": [["field", 5, null]],
    "order-by": [[["aggregation", 0], "desc"]]
  }
}

转换为 SQL:

SELECT "field_5", COUNT(*) as count, SUM("field_10") as sum
FROM "table_2"
GROUP BY "field_5"
ORDER BY count DESC

示例 3: JOIN 查询

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 1,
    "joins": [
      {
        "alias": "Products",
        "source-table": 2,
        "condition": [
          "=",
          ["field", 8, null],
          ["field", 15, {"join-alias": "Products"}]
        ]
      }
    ],
    "fields": [
      ["field", 5, null],
      ["field", 16, {"join-alias": "Products"}]
    ]
  }
}

转换为 SQL:

SELECT main."field_5", Products."field_16"
FROM "table_1" AS main
LEFT JOIN "table_2" AS Products ON main."field_8" = Products."field_15"

示例 4: 复杂表达式查询

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 2,
    "expressions": {
      "total_price": ["*", ["field", 10, null], ["field", 11, null]],
      "discounted": ["*", ["expression", "total_price"], 0.9]
    },
    "fields": [
      ["field", 9, null],
      ["expression", "total_price"],
      ["expression", "discounted"]
    ],
    "filter": [">", ["expression", "total_price"], 100]
  }
}

转换为 SQL:

SELECT 
  "field_9",
  "field_10" * "field_11" as total_price,
  ("field_10" * "field_11") * 0.9 as discounted
FROM "table_2"
WHERE ("field_10" * "field_11") > 100

SQL 转换机制

转换流程

MBQL JSON
    ↓
解析查询结构
    ↓
处理表达式定义 → 存储到 this.expressions
    ↓
转换 SELECT 子句 ← 处理 fields/aggregation/breakout
    ↓
转换 FROM 子句 ← 处理 source-table/joins
    ↓
转换 WHERE 子句 ← 处理 filter (递归)
    ↓
转换 GROUP BY 子句 ← 基于 breakout
    ↓
转换 ORDER BY 子句 ← 处理 order-by
    ↓
转换 LIMIT 子句 ← 处理 limit
    ↓
组装 SQL 字符串
    ↓
返回完整 SQL

核心转换类:MQLToSQL

class MQLToSQL {
  constructor(database, table, fieldMap, allTables) {
    this.database = database;
    this.table = table;
    this.fieldMap = fieldMap;      // 字段 ID → 字段对象
    this.allTables = allTables;    // 所有表 ID → 表对象
    this.expressions = {};         // 表达式定义
    this.joinAliases = {};         // JOIN 别名映射
    this.mainTableAlias = null;    // 主表别名
  }

  convert(mqlQuery) {
    // 转换入口
  }
}

SELECT 子句转换

逻辑流程

有 fields 指定?
├─ 是 → 有聚合/分组?
│      ├─ 是 → fields 过滤 breakout,所有 aggregation
│      └─ 否 → 直接使用 fields
└─ 否 → 有聚合/分组?
       ├─ 是 → 输出所有 breakout + aggregation
       └─ 否 → 输出 *

示例转换

场景 1: 无聚合,指定字段

{
  "fields": [["field", 1, null], ["field", 2, null]]
}

SELECT "name", "email"

场景 2: 有聚合,指定字段

{
  "aggregation": [["count"]],
  "breakout": [["field", 1, null], ["field", 2, null]],
  "fields": [["field", 1, null]]
}

SELECT "name", COUNT(*) as count

场景 3: 有聚合,无字段指定

{
  "aggregation": [["count"], ["sum", ["field", 3, null]]],
  "breakout": [["field", 1, null]]
}

SELECT "name", COUNT(*) as count, SUM("price") as sum

WHERE 子句转换

递归处理过滤条件树:

convertFilter(filter) {
  const [op, ...args] = filter;
  
  switch (op) {
    case '=':
    case '!=':
    case '>':
    case '<':
    case '>=':
    case '<=':
      return `${this.convertFieldOrExpression(args[0])} ${op} ${this.convertValue(args[1])}`;
    
    case 'and':
    case 'or':
      const conditions = args.map(arg => this.convertFilter(arg));
      return `(${conditions.join(` ${op.toUpperCase()} `)})`;
    
    case 'not':
      return `NOT (${this.convertFilter(args[0])})`;
    
    // ... 其他运算符
  }
}

JOIN 子句转换

convertFrom(query) {
  let from = `"${this.table.name}"`;
  
  if (this.mainTableAlias) {
    from += ` AS ${this.mainTableAlias}`;
  }
  
  if (query.joins) {
    query.joins.forEach(join => {
      const joinTable = this.allTables[join['source-table']];
      const alias = join.alias;
      this.joinAliases[alias] = joinTable;
      
      const condition = this.convertFilter(join.condition);
      from += ` LEFT JOIN "${joinTable.name}" AS ${alias} ON ${condition}`;
    });
  }
  
  return from;
}

表达式展开

表达式在多处使用时需要展开为实际 SQL:

convertExpression(exprName) {
  const expr = this.expressions[exprName];
  if (!expr) {
    throw new Error(`Expression not found: ${exprName}`);
  }
  
  const [op, ...args] = expr;
  
  if (['+', '-', '*', '/'].includes(op)) {
    const left = this.convertFieldOrExpression(args[0]);
    const right = this.convertValue(args[1]);
    return `(${left} ${op} ${right})`;
  }
  
  throw new Error(`Unsupported expression operator: ${op}`);
}

完整转换示例

MBQL 输入

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 1,
    "expressions": {
      "total": ["*", ["field", 10, null], ["field", 11, null]]
    },
    "aggregation": [["sum", ["expression", "total"]]],
    "breakout": [["field", 5, null]],
    "filter": [">", ["expression", "total"], 100],
    "order-by": [[["aggregation", 0], "desc"]],
    "limit": 10
  }
}

SQL 输出

SELECT 
  "customer_name", 
  SUM("quantity" * "price") as sum
FROM "orders"
WHERE ("quantity" * "price") > 100
GROUP BY "customer_name"
ORDER BY sum DESC
LIMIT 10

转换步骤

  1. 表达式处理:

    • "total"("quantity" * "price")
  2. SELECT 转换:

    • Breakout: ["field", 5, null]"customer_name"
    • Aggregation: ["sum", ["expression", "total"]]SUM("quantity" * "price") as sum
  3. FROM 转换:

    • "source-table": 1"orders"
  4. WHERE 转换:

    • [">", ["expression", "total"], 100]("quantity" * "price") > 100
  5. GROUP BY 转换:

    • 基于 breakout → GROUP BY "customer_name"
  6. ORDER BY 转换:

    • [["aggregation", 0], "desc"]ORDER BY sum DESC
  7. LIMIT 转换:

    • "limit": 10LIMIT 10

特殊处理

1. JOIN 字段引用

convertFieldReference(fieldRef) {
  const [type, fieldId, options] = fieldRef;
  
  if (options && options['join-alias']) {
    const alias = options['join-alias'];
    const joinTable = this.joinAliases[alias];
    const field = joinTable.fields.find(f => f.id === fieldId);
    return `${alias}."${field.name}"`;
  } else {
    const field = this.fieldMap[fieldId];
    const prefix = this.mainTableAlias || '';
    return prefix ? `${prefix}."${field.name}"` : `"${field.name}"`;
  }
}

2. 聚合结果排序

convertOrderBy(query) {
  if (!query['order-by']) return null;
  
  return query['order-by'].map(([fieldOrAgg, direction]) => {
    const [type, index] = fieldOrAgg;
    
    if (type === 'aggregation') {
      // 使用聚合别名
      const aggType = query.aggregation[index][0];
      return `${aggType} ${direction.toUpperCase()}`;
    } else {
      // 普通字段
      return `${this.convertFieldOrExpression(fieldOrAgg)} ${direction.toUpperCase()}`;
    }
  }).join(', ');
}

3. 值转换

convertValue(value) {
  if (value === null) {
    return 'NULL';
  } else if (typeof value === 'string') {
    return `'${value.replace(/'/g, "''")}'`;  // SQL 转义
  } else if (typeof value === 'number') {
    return value.toString();
  } else if (typeof value === 'boolean') {
    return value ? '1' : '0';
  }
  throw new Error(`Unsupported value type: ${typeof value}`);
}

项目结构

mintbase/
├── backend/                    # Node.js 后端
│   ├── src/
│   │   ├── index.js           # 应用入口,Express 服务器
│   │   ├── db/
│   │   │   └── index.js       # 数据库初始化和连接管理
│   │   ├── models/            # 数据模型
│   │   │   ├── card.js        # 问题(保存的查询)
│   │   │   ├── dashboard.js   # 仪表板
│   │   │   ├── database.js    # 数据库连接配置
│   │   │   ├── table.js       # 数据表元数据
│   │   │   └── field.js       # 字段元数据
│   │   ├── routes/            # API 路由
│   │   │   ├── index.js       # 路由汇总
│   │   │   ├── databases.js   # 数据库相关 API
│   │   │   ├── cards.js       # 问题相关 API
│   │   │   ├── dashboards.js  # 仪表板相关 API
│   │   │   └── query.js       # 查询执行 API
│   │   ├── query/             # 查询处理
│   │   │   ├── mbql-to-sql.js # MBQL → SQL 转换器 ⭐
│   │   │   ├── executor.js    # 查询执行器
│   │   │   └── validator.js   # 查询验证器
│   │   └── drivers/           # 数据库驱动
│   │       ├── index.js       # 驱动管理器
│   │       └── sqlite.js      # SQLite 驱动 ⭐
│   ├── scripts/
│   │   └── init_northwind.js  # Northwind 示例数据库初始化
│   ├── package.json
│   └── .env.example           # 环境变量示例
│
├── frontend/                  # 前端(原生 JS)
│   ├── pages/
│   │   ├── query-builder.html # 查询构建器 ⭐
│   │   ├── cards.html         # 问题列表
│   │   ├── card-detail.html   # 问题详情
│   │   ├── dashboards.html    # 仪表板列表
│   │   └── dashboard-view.html # 仪表板查看
│   ├── js/
│   │   ├── api.js             # API 客户端封装
│   │   ├── query-builder-ui.js # 查询构建器 UI 逻辑 ⭐
│   │   └── utils.js           # 工具函数
│   ├── css/
│   │   └── style.css          # Tailwind CSS(编译后)
│   └── package.json           # Tailwind CSS 依赖
│
├── data/                      # 数据文件
│   ├── mintbase.db           # 应用数据库(自动创建)
│   └── northwind.db          # 示例数据库(自动创建)
│
├── docs/                      # 文档
│   ├── architecture.md        # 架构设计文档
│   ├── query-language.md      # 查询语言详细文档
│   └── api.md                # API 接口文档
│
└── README.md                  # 本文件

⭐ 标记的是核心文件


快速开始

环境要求

  • Node.js: 18.x 或更高版本
  • npm: 9.x 或更高版本
  • 操作系统: Linux, macOS, Windows

安装步骤

1. 克隆项目

git clone https://github.com/caochun/mintbase.git
cd mintbase

2. 安装后端依赖

cd backend
npm install

3. 配置环境变量

cp .env.example .env

编辑 .env 文件:

# 服务器配置
PORT=3001
NODE_ENV=development

# CORS 配置
CORS_ORIGIN=*

# 数据库路径
APP_DB_PATH=../data/mintbase.db

4. 启动后端服务

npm start

服务将在 http://0.0.0.0:3001 启动。

5. 访问前端

打开浏览器访问:

http://localhost:3001

将自动跳转到查询构建器页面。

初始化示例数据

后端启动时会自动初始化 Northwind 示例数据库,包含以下表:

  • customers: 客户信息
  • employees: 员工信息
  • orders: 订单信息
  • order_details: 订单明细
  • products: 产品信息
  • categories: 产品分类
  • suppliers: 供应商信息

API 文档

基础 URL

http://localhost:3001/api

数据库相关

获取所有数据库

GET /api/databases

响应:

[
  {
    "id": 1,
    "name": "Northwind",
    "engine": "sqlite",
    "details": {
      "dbPath": "../data/northwind.db"
    }
  }
]

获取数据库表列表

GET /api/databases/:id/tables

响应:

[
  {
    "id": 1,
    "database_id": 1,
    "name": "customers",
    "schema": null
  }
]

获取表字段列表

GET /api/databases/:id/tables/:tableId/fields

响应:

[
  {
    "id": 1,
    "table_id": 1,
    "name": "customer_id",
    "base_type": "type/Integer",
    "semantic_type": "type/PK"
  }
]

获取 ER 图数据

GET /api/databases/:id/er-diagram

响应:

{
  "tables": {
    "1": {
      "id": 1,
      "name": "customers",
      "fields": [...]
    }
  },
  "relationships": [
    {
      "from_table_id": 2,
      "from_field": "customer_id",
      "to_table_id": 1,
      "to_field": "customer_id"
    }
  ]
}

获取表关系(智能 JOIN 推荐)

GET /api/databases/:id/tables/:tableId/relationships

响应:

{
  "outgoing": [
    {
      "table_id": 2,
      "table_name": "orders",
      "from_field": "customer_id",
      "to_field": "customer_id"
    }
  ],
  "incoming": []
}

查询相关

执行查询

POST /api/query
Content-Type: application/json

{
  "type": "query",
  "database": 1,
  "query": {
    "source-table": 1,
    "limit": 10
  }
}

响应:

{
  "data": {
    "rows": [[...], [...]],
    "cols": [
      {"name": "customer_id", "base_type": "type/Integer"},
      {"name": "customer_name", "base_type": "type/Text"}
    ]
  }
}

问题(Cards)相关

获取所有问题

GET /api/cards

创建问题

POST /api/cards
Content-Type: application/json

{
  "name": "客户总数",
  "description": "统计客户数量",
  "dataset_query": {...},
  "display": "bar",
  "visualization_settings": {...}
}

更新问题

PUT /api/cards/:id

删除问题

DELETE /api/cards/:id

仪表板相关

获取所有仪表板

GET /api/dashboards

创建仪表板

POST /api/dashboards
Content-Type: application/json

{
  "name": "销售分析",
  "description": "销售相关指标"
}

添加卡片到仪表板

POST /api/dashboards/:id/cards
Content-Type: application/json

{
  "card_id": 1,
  "row": 0,
  "col": 0,
  "size_x": 4,
  "size_y": 3
}

开发指南

添加新的数据库驱动

  1. backend/src/drivers/ 创建新驱动文件,如 mysql.js

  2. 实现驱动接口:

export class MySQLDriver {
  constructor(details) {
    this.details = details;
  }

  async connect() {
    // 建立连接
  }

  async getTables(database) {
    // 获取表列表
  }

  async getFields(database, tableName) {
    // 获取字段列表
  }

  async executeQuery(database, sql) {
    // 执行查询
  }

  disconnect() {
    // 关闭连接
  }
}
  1. backend/src/drivers/index.js 注册驱动:
import { MySQLDriver } from './mysql.js';

export function getDriver(engine) {
  switch (engine) {
    case 'sqlite':
      return SQLiteDriver;
    case 'mysql':
      return MySQLDriver;
    // ...
  }
}

添加新的聚合函数

  1. backend/src/query/mbql-to-sql.jsconvertAggregation 方法中添加:
convertAggregation(agg) {
  const [func, ...args] = agg;
  
  switch (func) {
    case 'count':
      return 'COUNT(*)';
    case 'sum':
      return `SUM(${this.convertFieldOrExpression(args[0])})`;
    case 'stddev':  // 新增标准差
      return `STDDEV(${this.convertFieldOrExpression(args[0])})`;
    // ...
  }
}
  1. 在前端 query-builder-ui.js 中添加 UI 选项:
const aggregationSelect = document.getElementById('aggregation-function');
aggregationSelect.innerHTML = `
  <option value="">选择聚合函数</option>
  <option value="count">计数</option>
  <option value="sum">求和</option>
  <option value="stddev">标准差</option>
`;

添加新的图表类型

  1. query-builder-ui.jsrenderChart 函数中添加处理逻辑:
function renderChart(result) {
  const chartType = document.getElementById('chart-type-select').value;
  
  switch (chartType) {
    case 'scatter':  // 新增散点图
      renderScatterChart(result);
      break;
    // ...
  }
}

function renderScatterChart(result) {
  // 使用 Chart.js 渲染散点图
}

调试技巧

查看生成的 SQL

在后端控制台会输出每次查询生成的 SQL:

Executing SQL: SELECT "customer_name", COUNT(*) as count FROM "orders" GROUP BY "customer_name"

前端调试

打开浏览器开发者工具(F12),在 Console 中查看:

  • MBQL 查询对象
  • API 请求和响应
  • 错误信息

后端调试

使用 VS Code 的调试功能:

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Backend",
      "program": "${workspaceFolder}/backend/src/index.js",
      "cwd": "${workspaceFolder}/backend",
      "envFile": "${workspaceFolder}/backend/.env"
    }
  ]
}

常见问题

Q: 为什么选择 SQLite 作为默认数据库?

A: SQLite 是文件型数据库,无需单独安装和配置服务器,非常适合快速开始和学习。你可以轻松添加对 PostgreSQL、MySQL 等的支持。

Q: 可以用于生产环境吗?

A: Mintbase 是一个教学项目,缺少很多生产环境必需的特性(如认证、权限、缓存、性能优化等)。建议仅用于学习和原型开发。

Q: 如何贡献代码?

A: 欢迎提交 Pull Request!请确保:

  1. 代码风格一致
  2. 添加必要的注释
  3. 更新相关文档

Q: 遇到 CORS 错误怎么办?

A: 检查 .env 文件中的 CORS_ORIGIN 配置,确保允许你的前端域名。

Q: 如何添加用户认证?

A: 可以集成 JWT 或 Session:

  1. 在后端添加认证中间件
  2. 实现登录/注册 API
  3. 在前端添加登录页面
  4. 在 API 请求中携带 token

许可证

MIT License

Copyright (c) 2024 Mintbase Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


致谢

  • Metabase: 灵感来源
  • Chart.js: 图表渲染
  • Mermaid.js: ER 图渲染
  • Tailwind CSS: UI 样式
  • Northwind Database: 示例数据

联系方式


Happy Coding! 🚀

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages