EmDash は データベース優先のコンテンツモデル を採用し、スキーマ定義はコードではなくデータベースに置かれます。ランタイムでのスキーマ変更と、開発者以外にも扱いやすいセットアップを可能にする根本的な設計です。
スキーマをデータとして
Strapi や Keystatic のような従来の CMS はコードでスキーマを定義します。
// 従来: コード内スキーマ
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);
どちらも同じコンテンツ構造を表します。違いはその構造の置き場所と変更方法です。
なぜデータベース優先か
ランタイム変更
コード変更や再ビルドなしでコンテンツタイプを作成・編集。開発者以外も管理 UI からデータモデルを設計できます。
実際の SQL 列
WordPress の EAV とは異なり各フィールドは実列を持ちます。インデックス、外部キー、クエリ最適化が適切に行えます。
自己文書化
DB ツールで直接スキーマを確認できます。コードを解析する必要はありません。
移行パス
バージョン管理用にスキーマを JSON エクスポート。新環境へインポート。
スキーマ用テーブル
システムテーブルが 2 つ、構造を定義します。
コレクションテーブル
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 | 管理 UI から作成 |
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
);
ランタイムのスキーマ変更
管理 UI でフィールドを追加すると EmDash は次を行います。
_emdash_fieldsにレコードを挿入ALTER TABLE ec_<collection> ADD COLUMNを実行- 検証用の Zod スキーマを再生成
SQLite がランタイムでサポートする ALTER TABLE 操作:
| 操作 | 対応 |
|---|---|
| 列追加 | はい |
| 列名変更 | はい |
| 列削除 | はい(SQLite 3.35+) |
| 型変更 | いいえ(テーブル再構築が必要) |
型変更時は EmDash が透過的に再構築します(新テーブル作成 → コピー → 旧削除 → リネーム)。
スキーマとコンテンツの分離
| 関心 | 場所 | テーブル |
|---|---|---|
| スキーマ | システムテーブル | _emdash_collections, _emdash_fields |
| コンテンツ | コレクションごとのテーブル | ec_posts, ec_products など |
| メディア | 別テーブル + ストレージ | media テーブル + R2/S3 |
| 設定 | オプションテーブル | site: プレフィックスの options |
この分離により:
- スキーマをコンテンツなしでエクスポート可能
- コンテンツをスキーマ間で移行可能
- システムテーブルにユーザーデータが混在しない
ランタイム検証
起動時に DB のフィールド定義から Zod スキーマを構築し、作成・更新のたびに検証します。
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 連携
データベーススキーマから TypeScript 型を生成します:
# スキーマを取得し型を生成
npx emdash types
.emdash/types.ts が生成されます:
// .emdash/types.ts(生成)
export interface Post {
title: string;
content: PortableTextBlock[];
excerpt?: string;
featuredImage?: string;
}
export interface Product {
title: string;
price: number;
quantity: number;
}
// クエリ関数の型付きオーバーロード
declare module "emdash" {
export function getEmDashCollection(
type: "posts",
): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;
export function getEmDashEntry(
type: "products",
id: string,
): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;
}
開発者と非開発者のワークフロー
開発者 は CLI を使用できます:
# スキーマ取得・型生成
npx emdash types
# スキーマを JSON でエクスポート
npx emdash export-seed > seed.json
非開発者 は管理 UI のみで操作します:
- 管理パネルの コンテンツタイプ を開く
- コレクション追加 をクリック
- ビジュアルビルダーでフィールドを定義
- すぐにコンテンツ作成を開始
どちらのアプローチも同じデータベーステーブルを更新します。
シードファイル
テンプレートとエクスポートでは、ポータブルなスキーマ定義に JSON シードファイルを使います:
{
"version": "1",
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" },
{ "slug": "featuredImage", "type": "image" }
]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "Primary Navigation" }]
}
プログラムからシードファイルを適用します:
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", // 'skip' | 'update' | 'error'
});
他方式との比較
| 方式 | スキーマ場所 | ランタイム変更 | 型 |
|---|---|---|---|
| EmDash | DB | あり(完全) | DB から生成 |
| WordPress | PHP + EAV | 限定的(メタ) | なし |
| Strapi | コード | なし(再ビルド) | ビルド時生成 |
| Sanity | コード | なし(デプロイ必須) | 組み込み |
| Directus | DB | あり(完全) | DB から生成 |
EmDash は Directus と同じモデル(データベース優先 + オプションの型生成)を採用しています。型安全な開発をサポートしつつ、最大限の柔軟性を提供します。