EmDash 採用資料庫優先的內容模型:schema 定義存放在資料庫而非程式碼中。這是支持執行時期改 schema、並讓非開發者也能上手的基礎設計。
Schema 即資料
Strapi、Keystatic 等傳統 CMS 常在程式碼裡定義 schema:
// 傳統方式:schema 寫在程式碼裡
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});
EmDash 把同樣的資訊存在表中:
-- _emdash_collections 表
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');
-- _emdash_fields 表
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);
兩種方式描述的是同一套內容結構,差別在於結構存放在哪、如何修改。
為何資料庫優先?
執行時期修改
無需改程式碼或重新建構即可增刪內容類型。非開發者也可在管理後台介面設計資料模型。
真實 SQL 列
不同於 WordPress 的 EAV,每個欄位對應真實列,便於索引、外鍵與查詢優化。
自描述
可用資料庫工具直接查看 schema,不必從程式碼反推資料模型。
遷移路徑
可將 schema 導出為 JSON 做版本管理,並在新環境中導入。
Schema 表
兩張系統表描述內容結構:
集合表
CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
label_singular TEXT,
description TEXT,
icon TEXT,
supports JSON,
source TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
source 記錄集合的來源:
| 值 | 說明 |
|---|---|
manual | 通過管理後台建立 |
template:blog | 來自模板種子文件 |
import:wordpress | 從 WordPress 導入 |
discovered | 從已有資料自動發現 |
欄位表
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL,
label TEXT NOT NULL,
type TEXT NOT NULL,
column_type TEXT NOT NULL,
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
內容表
每個集合有帶 ec_ 前綴的表。
CREATE TABLE ec_products (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft',
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
title TEXT NOT NULL,
price REAL
);
執行時期 schema 變更
在管理後台添加欄位時,EmDash 會:
- 在
_emdash_fields插入記錄 - 執行
ALTER TABLE ec_<collection> ADD COLUMN … - 重新生成用於驗證的 Zod schema
SQLite 在執行時期支持的 ALTER TABLE 操作:
| 操作 | 是否支持 |
|---|---|
| 添加列 | 是 |
| 重命名列 | 是 |
| 刪除列 | 是(SQLite 3.35+) |
| 修改列類型 | 否(需重建表) |
類型變更時,EmDash 會透明地完成表重建。
Schema 與內容分離
| 方面 | 位置 | 表 |
|---|---|---|
| Schema | 系統表 | _emdash_collections, _emdash_fields |
| 內容 | 按集合分表 | ec_posts, ec_products 等 |
| 媒體 | 獨立表 + 存儲 | media + R2/S3 |
| 設置 | 選項表 | 帶 site: 前綴的 options |
執行時期驗證
啟動時 EmDash 根據欄位定義建構 Zod schema,並在每次建立、更新時驗證。
function buildSchema(fields: Field[]): ZodSchema {
const shape: Record<string, ZodType> = {};
for (const field of fields) {
let zodType = fieldTypeToZod(field.type);
if (field.required) zodType = zodType.required();
if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
shape[field.slug] = zodType;
}
return z.object(shape);
}
與 TypeScript 整合
npx emdash types
會生成 .emdash/types.ts,並為 getEmDashCollection / getEmDashEntry 提供重載。
開發者與非開發者
開發者:npx emdash types、npx emdash export-seed > seed.json
非開發者:僅用管理後台 — 「內容類型」、添加集合、可視化欄位建構器。
兩者修改的是同一套底層表。
種子文件
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";
const { valid, errors } = validateSeed(seedData);
await applySeed(db, seedData, { includeContent: true, onConflict: "skip" });
與其他方案對比
| 方案 | Schema 位置 | 執行時期修改 | 類型 |
|---|---|---|---|
| EmDash | 資料庫 | 是(完整) | 由 DB 生成 |
| WordPress | PHP + EAV | 有限(元資料) | 無 |
| Strapi | 程式碼 | 否(需重建) | 建構時生成 |
| Sanity | 程式碼 | 否(需部署 schema) | 內置 |
| Directus | 資料庫 | 是(完整) | 由 DB 生成 |